JVM-(三)运行时数据区-堆中相关参数

本文最后更新于:May 13, 2023 pm

积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里,不积小流无以成江海。齐骥一跃,不能十步,驽马十驾,功不在舍。面对悬崖峭壁,一百年也看不出一条裂缝来,但用斧凿,能进一寸进一寸,能进一尺进一尺,不断积累,飞跃必来,突破随之。

目录

一个Java程序对应一个进程,一个进程对应一个JVM实例,一个JVM实例中只有一个运行时数据区,一个运行时数据区只有一个方法区和堆,一个进程中的多个线程共享同一个方法区和堆,每一个线程拥有独立的程序计数器、本地方法栈、虚拟机栈。

概述

  • 一个JVM实例只存在一个堆内存,堆也是Java内存管理的核心区域。Java堆区在JVM启动的时候即被创建,其空间大小也就确定了(但堆内存的大小是可以调节的)。堆是JVM管理的最大的一块内存空间。

  • 在《Java虚拟机规范》中规定,堆可以处于物理上不连续的内存空间中,但在逻辑上它应该被视为连续的。

  • 所有的线程共享Java堆,其中堆还可以划分线程私有的缓冲区(TLAB,Thread Local Allocation Buffer)。具体可见《JAVA知识点-Java对象的分配详解 》

实时可视化 Hotspot JVM 垃圾回收监控工具:安装插件 visual GC

堆内存结构

堆空间大小设置和查看

堆空间大小设置方式

Java堆的大小在JVM启动时就已经设定好了,但可以通过选项 “-Xmx“和”-Xms“来进行设置。

  • -Xms:表示堆的起始内存大小,等价于 -XX:InitialHeapSize
  • -Xmx:表示堆的最大内存,等价于 -XX:MaxHeapSize

-X是JVM的运行参数,ms表示memory start。

一旦堆区中的内存大小超过了”-Xmx”所指定的最大内存时,将会抛出OutOfMemoryError异常。

  • 通常会将”-Xms”和”-Xmx”两个参数配置相同的值,目的是:为了能够在Java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小,从而提高性能。

默认大小

  • 初始内存大小:物理电脑内存大小 / 64。(64分之一)
  • 最大内存大小:物理电脑内存大小 / 4。(4分之一)

查看本机内存情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.tothefor;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;

/**
* @Author DragonOne
* @Date 2022/6/5 21:12
* @墨水记忆 www.tothefor.com
*/
public class TestLink {
public static void main(String[] args) {
long initM = Runtime.getRuntime().totalMemory() / 1024 / 1024;
long MaxM = Runtime.getRuntime().maxMemory() / 1024 / 1024;

System.out.println("-Xms: "+initM+"M");
System.out.println("-Xmx: "+MaxM+"M");

System.out.println("系统内存: "+initM*64.0/1024+"G");
System.out.println("系统内存: "+MaxM*4.0/1024+"G");
}
}

手动设置

设置方式可以见《JVM-(二)运行时数据区-线程共享 》

1
-Xms600m -Xmx600m

然后再次跑上面的代码查看内存情况,可以发现前两个已经发生了改变。后两个是电脑的内存,所以不变。但是输出的数字并不是设置的数字,为什么呢?往后看。(我显示的是575M)

开发中建议将初始堆内存和最大的堆内存设置成相同的值

  • 原因:在初始设置后,如果空间不够时将会进行扩容(没有超过最大内存),当空间有空闲了则又会进行空间的释放。当频繁的进行扩容和释放时,就会造成系统过大的压力。而设置成相同的,就避免了进行空间的大小调整。

数据显示和设置不同

主要是计算的问题。因为幸存区有两个区,但是计算的时候只算了一个区。下面就这两种情况的计算过程进行演示说明:

空间大小设置说明:-Xms600m -Xmx600m,最后输出575M

然后跑以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.tothefor;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;

/**
* @Author DragonOne
* @Date 2022/6/5 21:12
* @墨水记忆 www.tothefor.com
*/
public class TestLink {
public static void main(String[] args) {
long initM = Runtime.getRuntime().totalMemory() / 1024 / 1024;
long MaxM = Runtime.getRuntime().maxMemory() / 1024 / 1024;

System.out.println("-Xms: "+initM+"M");
System.out.println("-Xmx: "+MaxM+"M");
try {
Thread.sleep(1000000); //主要目的是让程序一直处于运行状态
}catch (Exception e){
e.printStackTrace();
}
}
}

然后再打开cmd窗口(我是Mac系统但命令都一样),输入:(前提是配置好了Java环境)

1
2
3
4
5
6
7
8
jps

# 显示
4202 Jps
3916
3950 RemoteMavenServer36
4158 Launcher
4159 TestLink # 这个就是我们刚才跑的代码的类名,记住这个进程id,下面需要用

再次输入:jstat -gc xxx(上面的进程id)

1
2
3
4
5
jstat -gc 4159

# 显示
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT CGC CGCT GCT
25600.0 25600.0 0.0 0.0 153600.0 15360.5 409600.0 0.0 4480.0 781.7 384.0 76.6 0 0.000 0 0.000 - - 0.000

因为输出的结果很多,所以比较长。这里我自己画了一个图:

我们只看左边(新生代),不看右边。其中以C结尾的表示总大小,以U结尾的表示已使用的大小。

S0C 、 S1C 表示幸存区S0和S1两个区域的大小,EC表示伊甸区总大小,OC表示老年代的总大小。

三者相加为:(25600+25600+153600+409600)/ 1024 = 600

但是真正计算的时候幸存区的两个区域只需要计算一个:

(25600+153600+409600)/ 1024 = 575

所以,这就是计算的过程。

查看方式二

通过设置参数:-XX:+PrintGCDetails 。打印GC过程的细节。

1
-Xms600m -Xmx600m -XX:+PrintGCDetails
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.tothefor;

import org.springframework.beans.factory.FactoryBean;
import org.springframework.context.ApplicationContext;

/**
* @Author DragonOne
* @Date 2022/6/5 21:12
* @墨水记忆 www.tothefor.com
*/
public class TestLink {
public static void main(String[] args) {
long initM = Runtime.getRuntime().totalMemory() / 1024 / 1024;
long MaxM = Runtime.getRuntime().maxMemory() / 1024 / 1024;

System.out.println("-Xms: "+initM+"M");
System.out.println("-Xmx: "+MaxM+"M");
}
}


//输出
-Xms: 575M
-Xmx: 575M
Heap
PSYoungGen total 179200K, used 9216K [0x00000007b3800000, 0x00000007c0000000, 0x00000007c0000000)
eden space 153600K, 6% used [0x00000007b3800000,0x00000007b41001a0,0x00000007bce00000)
from space 25600K, 0% used [0x00000007be700000,0x00000007be700000,0x00000007c0000000)
to space 25600K, 0% used [0x00000007bce00000,0x00000007bce00000,0x00000007be700000)
ParOldGen total 409600K, used 0K [0x000000079a800000, 0x00000007b3800000, 0x00000007b3800000)
object space 409600K, 0% used [0x000000079a800000,0x000000079a800000,0x00000007b3800000)
Metaspace used 3134K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 337K, capacity 388K, committed 512K, reserved 1048576K

其中的from和to就是幸存区的S0和S1。这里的数据和上面通过命令的方式获取到的数据是一样的。

注意:PSYoungGen的值是eden +(from或者to)。

新生代和老年代相关参数设置

新生代又分为Eden区,Survivor0区、Survivor1区(又称为from区和to区)。新生代和老年代的比例为1:2;新生代中,Eden:Survivor0:Survivor1为 8:1:1。

默认新生代和老年代在堆结构的占比

  • 默认 -XX:NewRatio=2,表示新生代占1,老年代占2。新生代占堆的1/3。
  • 修改为 -XX:NewRatio=4,表示新生代占1,老年代占4。新生代占堆的1/5。

查看新生代和老年代默认占比

配置参数:

1
-Xms600m -Xmx600m

运行代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.tothefor;


/**
* @Author DragonOne
* @Date 2022/6/5 21:12
* @墨水记忆 www.tothefor.com
*/
public class TestLink {
public static void main(String[] args) {
long initM = Runtime.getRuntime().totalMemory() / 1024 / 1024;
long MaxM = Runtime.getRuntime().maxMemory() / 1024 / 1024;

System.out.println("-Xms: "+initM+"M");
System.out.println("-Xmx: "+MaxM+"M");
try {
Thread.sleep(1000000000);
}catch (Exception e){
e.printStackTrace();
}
}
}

然后看插件监控情况:

可以看见150+25+25 :400 就是1:2。

默认情况下不会修改这个比例,但是,如果已经知道了存活期长的比较多,那么就可以将老年代比例设大。

新生代分布占比

Eden区和幸存区的两个区域所占比例为8:1:1。可以通过 -XX:SurvivorRatio 调整比例。默认值为8。可以通过命令:

1
2
3
jinfo -flag SurvivorRatio 4995 # 4995为进程id,通过jps命令查看所有

# -XX:SurvivorRatio=8

但是根据上面插件的监测情况看,实际比例为125:25:25。这是为什么呢?显示为8,但实际不一样?

这主要是和自适用机制有关。默认情况下有一个自适应比例。可以通过设置参数来关闭自适应比例。

1
-XX:-UseAdaptiveSizePolicy #:后面的-号表示关闭,使用则为+号

📢这样配置后,是没有效果的。

要想真正的实现效果,必须是显示指定 -XX:SurvivorRatio=8

1
-Xms600m -Xmx600m -XX:+PrintGCDetails -XX:SurvivorRatio=8

这样配置后再次查看,就会发现Eden变成了160M,而幸存区中都是20M了,这样就符合8:1:1的比例了。(因为参数设置的总大小600,新生代:老年代为1:2;所以新生代中总大小为200M)如图:

参数总结

堆空间大小参数

  • -Xms:表示堆的起始内存大小,等价于 -XX:InitialHeapSize
  • -Xmx:表示堆的最大内存,等价于 -XX:MaxHeapSize

通过设置参数:-XX:+PrintGCDetails 。打印GC过程的细节。

新生代和老年代在堆结构的占比

  • 默认 -XX:NewRatio=2,表示新生代占1,老年代占2。新生代占堆的1/3。

新生代分布占比

  • 通过 -XX:SurvivorRatio 调整比例,默认为8。但需要注意自适应机制带来的变化。