深入理解 Java 虚拟机 -- 内存分代策略及常用参数配置

本贴最后更新于 2122 天前,其中的信息可能已经时过境迁

内存分代策略

简介

HotSpot 中为了提高内存对象内存分配和垃圾回收的效率,将内存分为了新生代(Eden+From Survivor+To Survivor)、老年代(OldGen)和永久代(PermGen)。新创建的对象分配在新生代,多次回收还存活的对象放在老年代,类信息、静态变量和字符串常量等存放在永久代中。新生代中需要频繁执行垃圾回收,老年代中不需要频繁垃圾回收,永久代一般来说不实现垃圾回收。这样对不同的区域实行不同的垃圾回收算法,可以提高垃圾回收效率。
在 Java7 之前,方法区在永久代,永久代和堆隔离,使用的是堆空间,永久代大小在启动 JVM 时设置一个固定的值,不可变;Java7 中将 static 从永久代移到堆中;Java8 中取消永久代,使用元空间(Metaspace)替代,与堆共享物理内存,逻辑上可以认为和堆在一起。元空间使用的是系统内存,因此有足够空间。
图片来源网络

新生代

新生代垃圾回收效率高,Minor GC 是新生代的 GC,回收速度快,Eden 空间不足时会触发 Minor GC 进行回收操作。新生代分为三块空间,一个 Eden 和两个 Survivor(通常称为 To Survivor 和 From Survivor),当 Eden 被填满,会执行 Minor GC,将存活下来的对象转移到 To Survivor,From Survivor 中存活的对象,判断年龄阈值(默认 15,一轮 GC 表示增加一岁),如果超过阈值就放到老年代中,没超过的存到 To Survivor 中,然后将 To Survivor 和 From Survivor 互换,也就是 To Survivor 在 GC 之后永远是空的。

老年代

老年代包含长期存活的对象和多次 Minor GC 后存活下来的对象,通常老年代被占满或者显式调用 System.gc()方法才开始垃圾回收,老年代垃圾回收也就叫 Full GC/Major GC,

JVM 常用参数配置

了解了内存分代策略,下面可以学习一下常用的参数配置,并查看不同内存区域的变化情况。

怎样设置 JVM 参数

方法很多,可以上网查询,我这里说我使用的 Eclipse 中设置方式。
在运行 Java 的按钮下面,Run As 下面,有一个 Run Configurations->Arguments->VMarguments
QQ20190130141444jpg

Trace 跟踪参数

Trace 跟踪参数就是跟踪 GC 运行的参数。

打印 GC 简要信息
-verbose:gc  
-XX:+PrintGC

-verbose:gc 表示输出虚拟机中 GC 的情况
-XX:+PrintGC 功能和-verbose:gc 一样

    Object obj = new byte[1*1024*1024];
	obj = null;
	System.gc();
	/* 输出:
	[GC (System.gc())  3020K->704K(125952K), 0.0008022 secs]
    [Full GC (System.gc())  704K->514K(125952K), 0.0049984 secs]
	*/

上面的代码,将 obj 设置为 null,让 GC 回收创建的 BYTE 数组,是属于新生代的 Minor GC。调用 System.gc(),是 Full GC

打印 GC 详细信息
-XX:+PrintGCDetails     //打印GC详细信息
-XX:+PrintGCTimeStamps  //打印GC发生的时间戳 
-XX:+PrintGCDetails的输出:
Heap
 PSYoungGen      total 38400K, used 3686K [0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)
  eden space 33280K, 11% used [0x00000000d5f00000,0x00000000d6299b30,0x00000000d7f80000)
  from space 5120K, 0% used [0x00000000d8480000,0x00000000d8480000,0x00000000d8980000)
  to   space 5120K, 0% used [0x00000000d7f80000,0x00000000d7f80000,0x00000000d8480000)
 ParOldGen       total 87552K, used 0K [0x0000000081c00000, 0x0000000087180000, 0x00000000d5f00000)
  object space 87552K, 0% used [0x0000000081c00000,0x0000000081c00000,0x0000000087180000)
 Metaspace       used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 280K, capacity 386K, committed 512K, reserved 1048576K

下面我简单介绍一下里面的参数:

PSYoungGen      total 38400K, used 3686K [0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)

第一行的 PSYoungGen 表示新生代;total 38400K 表示总大小为 38400K;used 3686K 表示已经使用了 3686K;[0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)分别表示当前区域在内存中的位置,分别是低边界,当前边界和高边界,(0x00000000d8980000-0x00000000d5f00000)/1024/1024 = 42M,
33280K+5120K+5120K=42M(eden space+from space + to space)

文件输出 GC log

有时候在运行环境下,需要查看错误信息,就需要将 log 文件保存在本地,应该使用下面的命令:

-Xloggc:F:/gc.log

这样在 F 盘就出现 gc.log 文件,打开信息为:

Java HotSpot(TM) 64-Bit Server VM (25.192-b12) for windows-amd64 JRE (1.8.0_192-b12), built on Oct  6 2018 17:12:23 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8272984k(2132632k free), swap 16544068k(9949684k free)
CommandLine flags: -XX:InitialHeapSize=132367744 -XX:MaxHeapSize=2117883904 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC 
0.100: [GC (System.gc()) [PSYoungGen: 3020K->664K(38400K)] 3020K->672K(125952K), 0.0009640 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
0.101: [Full GC (System.gc()) [PSYoungGen: 664K->0K(38400K)] [ParOldGen: 8K->514K(87552K)] 672K->514K(125952K), [Metaspace: 2627K->2627K(1056768K)], 0.0054643 secs] [Times: user=0.08 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 38400K, used 333K [0x00000000d5f00000, 0x00000000d8980000, 0x0000000100000000)
  eden space 33280K, 1% used [0x00000000d5f00000,0x00000000d5f534a8,0x00000000d7f80000)
  from space 5120K, 0% used [0x00000000d7f80000,0x00000000d7f80000,0x00000000d8480000)
  to   space 5120K, 0% used [0x00000000d8480000,0x00000000d8480000,0x00000000d8980000)
 ParOldGen       total 87552K, used 514K [0x0000000081c00000, 0x0000000087180000, 0x00000000d5f00000)
  object space 87552K, 0% used [0x0000000081c00000,0x0000000081c80ba0,0x0000000087180000)
 Metaspace       used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 280K, capacity 386K, committed 512K, reserved 1048576K
监控加载了哪些类
-XX:+TraceClassLoading

使用这个参数能获取到 Java 加载到的类。

[Opened D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.Object from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.io.Serializable from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.Comparable from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.CharSequence from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.String from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.reflect.AnnotatedElement from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.reflect.GenericDeclaration from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
[Loaded java.lang.reflect.Type from D:\myeclipse\binary\jdk1.8\jre\lib\rt.jar]
.......
打印类的信息
-XX:+PrintClassHistogram

按下 Ctrl+Break 后,打印类的信息(四个参数分别是:序号、实例数量、总大小、类型
):

 num     #instances         #bytes  class name
----------------------------------------------
   1:        890617      470266000  [B
   2:        890643       21375432  java.util.HashMap$Node
   3:        890608       14249728  java.lang.Long
   4:            13        8389712  [Ljava.util.HashMap$Node;
   5:          2062         371680  [C
   6:           463          41904  java.lang.Class

堆的分配参数

指定最大堆和最小堆

指定参数:

-Xmx
-Xms
//下面表示最大堆为20m,最小堆是5m
-Xmx20m
-Xms5m

获取 JVM 最大堆数据:

    //获取最大堆大小
	System.out.print("Xmx=");
	System.out.println(Runtime.getRuntime().maxMemory()/1024.0/1024+"M");
    //获取空闲内存大小
	System.out.print("free mem=");
	System.out.println(Runtime.getRuntime().freeMemory()/1024.0/1024+"M");
    //获取总计内存大小
	System.out.print("total mem=");
	System.out.println(Runtime.getRuntime().totalMemory()/1024.0/1024+"M");

输出结果:

Xmx=1796.0M  
free mem=121.04998016357422M  
total mem=123.0M  

也就是说,我的 JVM 设置最大堆是 1796.0M ,总计 123.0M ,还有 121.04998016357422M 可用。那么设置一下-Xmx20m -Xms5m 参数,再看一下结果:

Xmx=18.0M
free mem=4.761444091796875M
total mem=5.5M

如果分配 1M 的数组,再次执行:

byte[] b = new byte[1*1024*1024];

分配 1M 数组后,空闲空间变少:

Xmx=18.0M
free mem=3.76141357421875M
total mem=5.5M

如果空闲空间全部使用完会怎么样呢?那我们创建一个 5M 的数组,看一下结果:

byte[] b = new byte[5*1024*1024];

输出的结果变成了:

Xmx=18.0M
free mem=5.26141357421875M
total mem=11.0M

这里的最大堆没变,还是 18M,总的内存大小从 5.5M 变成了 11M,也就是说如果空闲空间不能支撑对象所需容量,那么就会扩容。

堆的其他参数

设置新生代大小(绝对参数,设置多少就是多少):

-Xmn

新生代(eden+2*s)和老年代(不包含永久区)的比值:
4 表示 新生代:老年代=1:4,即年轻代占堆的 1/5

-XX:NewRatio

设置两个 Survivor 区和 eden 的比:
8 表示 两个 Survivor :eden=2:8,即一个 Survivor 占年轻代的 1/10

-XX:SurvivorRatio

我们测试一下堆内空间的变化,创建一个 10M 的数组,数组是循环创建:

    byte[] b=null;
    for(int i=0;i<10;i++)
    	b=new byte[1*1024*1024];

先看一下新生代分配 1M 空间:

配置参数:-Xmx20m -Xms20m -Xmn1m  -XX:+PrintGCDetails 
[GC (Allocation Failure) [PSYoungGen: 507K->504K(1024K)] 507K->504K(19968K), 0.0010025 secs] [Times: user=0.05 sys=0.02, real=0.00 secs] 
Heap
 PSYoungGen      total 1024K, used 721K [0x00000000ffe80000, 0x0000000100000000, 0x0000000100000000)
  eden space 512K, 42% used [0x00000000ffe80000,0x00000000ffeb6790,0x00000000fff00000)
  from space 512K, 98% used [0x00000000fff00000,0x00000000fff7e030,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 18944K, used 10240K [0x00000000fec00000, 0x00000000ffe80000, 0x00000000ffe80000)
  object space 18944K, 54% used [0x00000000fec00000,0x00000000ff6000a0,0x00000000ffe80000)
 Metaspace       used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 280K, capacity 386K, committed 512K, reserved 1048576K
Java HotSpot(TM) 64-Bit Server VM warning: NewSize (1536k) is greater than the MaxNewSize (1024k). A new max generation size of 1536k will be used.

我用的 JDK1.8,这里还报了 Java HotSpot(TM) 64-Bit Server VM warning,主要是因为新生代的空间不足,不能分配。我们可以看到,新生代空间不足 1M,因此新生代无法分配,就在老年代里面分配了 10M 空间。新生代发生了一次 GC,回收了 3K 空间,可以忽略不计。那么继续增加新生代空间到 15M:

执行参数:-Xmx20m -Xms20m -Xmn15m  -XX:+PrintGCDetails

Heap
 PSYoungGen      total 13824K, used 11525K [0x00000000ff100000, 0x0000000100000000, 0x0000000100000000)
  eden space 12288K, 93% used [0x00000000ff100000,0x00000000ffc41760,0x00000000ffd00000)
  from space 1536K, 0% used [0x00000000ffe80000,0x00000000ffe80000,0x0000000100000000)
  to   space 1536K, 0% used [0x00000000ffd00000,0x00000000ffd00000,0x00000000ffe80000)
 ParOldGen       total 5120K, used 0K [0x00000000fec00000, 0x00000000ff100000, 0x00000000ff100000)
  object space 5120K, 0% used [0x00000000fec00000,0x00000000fec00000,0x00000000ff100000)
 Metaspace       used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 280K, capacity 386K, committed 512K, reserved 1048576K

这里没有发生 GC,新生代使用了 11525K 空间,说明新生代空间足够分配,因此没有触发 GC。那么将新生代空间调整到 7M 会发生什么呢?

执行参数:-Xmx20m -Xms20m -Xmn7m  -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 6036K->480K(6656K)] 6036K->1648K(19968K), 0.0012152 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 6656K, used 5782K [0x00000000ff900000, 0x0000000100000000, 0x0000000100000000)
  eden space 6144K, 86% used [0x00000000ff900000,0x00000000ffe2d900,0x00000000fff00000)
  from space 512K, 93% used [0x00000000fff00000,0x00000000fff78020,0x00000000fff80000)
  to   space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
 ParOldGen       total 13312K, used 1168K [0x00000000fec00000, 0x00000000ff900000, 0x00000000ff900000)
  object space 13312K, 8% used [0x00000000fec00000,0x00000000fed24020,0x00000000ff900000)
 Metaspace       used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 280K, capacity 386K, committed 512K, reserved 1048576K

这里发生了一次新生代 GC,回收了 6036K-1648k= 4388k 空间,剩下的 5782k 在新生代。继续调整,这次调整一下新生代中 eden 和 Survivor 的比例,设置-XX:SurvivorRatio=2,增大了 Survivor 的大小。

执行参数:-Xmx20m -Xms20m -Xmn7m   -XX:SurvivorRatio=2 -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 3922K->1504K(5632K)] 3922K->1624K(18944K), 0.0025991 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 4656K->1504K(5632K)] 4776K->1624K(18944K), 0.0114028 secs] [Times: user=0.08 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [PSYoungGen: 4646K->1504K(5632K)] 4766K->1632K(18944K), 0.0024505 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 5632K, used 2610K [0x00000000ff900000, 0x0000000100000000, 0x0000000100000000)
  eden space 4096K, 27% used [0x00000000ff900000,0x00000000ffa14930,0x00000000ffd00000)
  from space 1536K, 97% used [0x00000000ffd00000,0x00000000ffe78030,0x00000000ffe80000)
  to   space 1536K, 0% used [0x00000000ffe80000,0x00000000ffe80000,0x0000000100000000)
 ParOldGen       total 13312K, used 128K [0x00000000fec00000, 0x00000000ff900000, 0x00000000ff900000)
  object space 13312K, 0% used [0x00000000fec00000,0x00000000fec20010,0x00000000ff900000)
 Metaspace       used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 280K, capacity 386K, committed 512K, reserved 1048576K

发生了 3 次新生代的 GC,都是新生代的 Minor GC,第一次 GC 回收了 3922K-1624K=2298k,第二次 GC 回收了 4776K-1624K=3152K,第三次 GC 回收了 4766K-1632K=3134k,剩下的 2610k 在新生代中,还有一部分去了 From Survivor 中。再看一下,-XX:NewRatio=1 的情况:

执行参数:-Xmx20m -Xms20m -XX:NewRatio=1    -XX:SurvivorRatio=2 -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 4932K->1688K(7680K)] 4932K->1696K(17920K), 0.0011783 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [PSYoungGen: 5884K->1592K(7680K)] 5892K->1600K(17920K), 0.0009533 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 7680K, used 3834K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 5120K, 43% used [0x00000000ff600000,0x00000000ff830ad8,0x00000000ffb00000)
  from space 2560K, 62% used [0x00000000ffd80000,0x00000000fff0e040,0x0000000100000000)
  to   space 2560K, 0% used [0x00000000ffb00000,0x00000000ffb00000,0x00000000ffd80000)
 ParOldGen       total 10240K, used 8K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 0% used [0x00000000fec00000,0x00000000fec02000,0x00000000ff600000)
 Metaspace       used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 280K, capacity 386K, committed 512K, reserved 1048576K

可以看到,提高新生代空间之后,发生了两次 GC,因为新生代能分配更多的空间,避免 GC,也没有是用老年代的空间。那么再增加 eden 空间会怎么样呢?

执行参数:-Xmx20m -Xms20m -XX:NewRatio=1   -XX:SurvivorRatio=3 -XX:+PrintGCDetails

[GC (Allocation Failure) [PSYoungGen: 6036K->1720K(8192K)] 6036K->1728K(18432K), 0.0059791 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 8192K, used 7022K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 6144K, 86% used [0x00000000ff600000,0x00000000ffb2d900,0x00000000ffc00000)
  from space 2048K, 83% used [0x00000000ffc00000,0x00000000ffdae040,0x00000000ffe00000)
  to   space 2048K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x0000000100000000)
 ParOldGen       total 10240K, used 8K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 0% used [0x00000000fec00000,0x00000000fec02000,0x00000000ff600000)
 Metaspace       used 2633K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 280K, capacity 386K, committed 512K, reserved 1048576K

提高 Eden 空间之后,只发生了一次 GC,GC 越少,说明系统执行效率越高。

OOM 内存溢出
-XX:+HeapDumpOnOutOfMemoryError
OOM时导出堆到文件
-XX:+HeapDumpPath
导出OOM的路径
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
当发生OOM时,导出到文件a.dump
-XX:OnOutOfMemoryError
在OOM时,执行一个脚本
"-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p"
当程序OOM时,在D:/a.txt中将会生成线程的dump
可以在OOM时,发送邮件,甚至是重启程序
默认配置

新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2
Edem : from : to = 8 : 1 : 1

永久区分配参数

-XX:PermSize  -XX:MaxPermSize
设置永久区的初始空间和最大空间

使用 CGLIB 等库的时候,可能会产生大量的类,这些类,有可能撑爆永久区导致 OOM。

栈空间分配

-Xss

通常只有几百 K,决定了函数调用的深度;
每个线程都有独立的栈空间,局部变量、参数 分配在栈上.
因为每个线程都需要分配一个栈空间,因此如果想多跑一些线程,就需要将这个值调小,容纳更多线程。

参考资料

1.JAVA 方法区是在堆里面吗
2.Java 虚拟机:JVM 内存分代策略

  • B3log

    B3log 是一个开源组织,名字来源于“Bulletin Board Blog”缩写,目标是将独立博客与论坛结合,形成一种新的网络社区体验,详细请看 B3log 构思。目前 B3log 已经开源了多款产品:SymSoloVditor思源笔记

    1063 引用 • 3453 回帖 • 203 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...