JVM基础 -- 晋升规则
本文将通过最
基本的垃圾收集器(Serial+Serial Old),简单地讲述JVM内存分配和回收过程中的3个基本的晋升规则:大对象直接晋升、对象年龄晋升、动态晋升
代码托管在:https://github.com/zhongmingmao/jvm_demo
-XX:+UseSerialGC
新生代采用Serial垃圾收集器(Copying算法,单线程,Stop The World)老年代采用Serial Old垃圾收集器(Mark-Compact算法,单线程,Stop The World)
Minor GC / Major GC / Full GC
常规理解
Eden空间不够 ➔Minor GC➔ 回收Young GenerationOld Generation空间不够 ➔Major GC➔ 回收Old GenerationMethod Area(Java 8开始由MetaSpace实现,之前由Permanent Generation实现)空间不够 ➔Full GC➔ 回收Young Generation+Old Generation+Method Area
最难区分的是Major GC和Full GC,其实并没有明确规定两者的区别,因此不要以Minor GC、Major GC、Full GC的方式来思考问题,而应该关注
GC是否需要Stop-The-WorldGC能否并发(不是并行,是并发!)- 应用的
延迟和吞吐量
本文认为Major GC ≈ Full GC,不作区分
Minor GC
- 发生在
新生代,当Eden区域没有足够空间进行分配 - Java对象大多具有
短命的特性 Minor GC非常频繁,速度也比较快Minor GC会造成Stop-The-World,但由于新生代的对象为大多为短命对象,因此由Stop-The-World而造成的延迟可以忽略
Major GC / Full GC
- 发生在
老年代 - 出现
Major GC,经常伴随至少一次Minor GC - 速度比较
慢,SpeedOf(Minor GC) ≈ 10 * SpeedOf(Major GC) Major GC也会造成Stop-The-World,但由于老年代的对象为大多为长命对象,因此由Stop-The-World而造成的延迟可能会比较大,因此才会出现并发收集器:CMS和G1(Java 9默认)
大对象直接晋升
代码
1 | // JVM Args : -Xms20m -Xmx20m -Xmn10m -XX:SurvivorRatio=8 -XX:+UseSerialGC -XX:+PrintGCDetails |
运行结果
1 | [Full GC (System.gc()) [Tenured: 0K->467K(10240K), 0.0037172 secs] 2217K->467K(19456K), [Metaspace: 3177K->3177K(1056768K)], 0.0037645 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
运行过程
System.gc()
对应的GC日志
1 | [Full GC (System.gc()) [Tenured: 0K->467K(10240K), 0.0037172 secs] 2217K->467K(19456K), [Metaspace: 3177K->3177K(1056768K)], 0.0037645 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
- 进行
Full GC的原因是代码调用System.gc() 堆总大小为19456K≈20M老年代(Tenured Generation)总大小为10240K=10M,目前占用467K,可忽略新生代(Def New Generation)目前占用467K-467K=0K,总大小 = 堆大小 - 老年代大小 ≈10M,SurvivorRatio=8,因此Eden区大小为8M,Survivor区大小为1M
byte[] b1 = new byte[5 * _1MB]
Eden区有8M空闲空间,能完全容纳b1,不会进行GC
byte[] b2 = new byte[5 * _1MB]
对应的GC日志
1 | [GC (Allocation Failure) [DefNew: 5447K->4K(9216K), 0.0036997 secs] 5915K->5592K(19456K), 0.0037186 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
1. `Eden`区只有`8M`,此时已经分配了`b1`,最多只剩下`3M`的可用空间,`Serial`采集器采用`Copying`算法,不足以容纳`b2`,触发`Minor GC`
2. 进行`Minor GC`的原因是`Allocation Failure`,即内存分配失败,与上面分析一致
3. 进行`Minor GC`,首先尝试将`Eden`区和`Survivor 0`区中的`存活对象`一起移到`Survivor 1`区,`b1`是强引用,依旧存活,但由于`Survivor 1`区只有`1M`,无法容纳`b1`,尝试将`b1`直接晋升到`Tenured`区
4. 此时`Tenured`有`10M`的可用空间,能容纳`b1`,可以将`b1`晋升到`Tenured`区,并释放`b1`原本在`Eden`区占用的内存空间
5. `Minor GC`结束后,`b2`便能顺利地在`Eden`区进行分配
byte[] b3 = new byte[5 * _1MB]
对应的GC日志
1 | [GC (Allocation Failure) [DefNew: 5209K->5209K(9216K), 0.0000165 secs][Tenured: 5587K->5587K(10240K), 0.0029173 secs] 10796K->10713K(19456K), [Metaspace: 3270K->3270K(1056768K)], 0.0029714 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
- 由于
Eden区此时最多有3M的可用空间,要分配b3,首先触发一次Minor GC - 由于内存空间(
Eden、Survivor 0、Tenured)不足,Minor GC也会失败,进而触发Full GC Tenured区采用Serial Old收集器,Full GC时会尝试采用Mark-Compact算法进行GC,但依旧会失败,进而导致抛出OutOfMemoryError
小结
尽量避免使用**需要连续内存空间的短命大对象**,例如长字符串和基本类型的大数组,因为这会导致Minor GC的时候,这些短命大对象有可能会直接晋升到老年代,进而加大Full GC的频率
对象年龄晋升
对象每经历一次Minor GC,年龄就会增加1,到了一定的阈值(-XX:MaxTenuringThreshold,默认是15),就会晋升到老年代
代码
1 | /** |
运行结果
1 | [Full GC (System.gc()) [Tenured: 0K->452K(102400K), 0.0032760 secs] 6555K->452K(194560K), [Metaspace: 3198K->3198K(1056768K)], 0.0033038 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
byte[] b = new byte[_10MB / 4]
1 | Init Address : 33076281344 |
尝试分配对象b到**Eden**区,内存空间充足,初始内存地址为33076281344,GC年龄为0
循环i=0
1 | Address[0] : 33076281344 , GC Age : 0 |
尝试在Eden区分配5*_10MB的内存空间,内存空间充足,不需要触发Minor GC,对象b的内存地址不变,依旧是33076281344,在**Eden**区,GC年龄为0
循环i=1
1 | [GC (Allocation Failure) [DefNew: 70169K->3565K(92160K), 0.0051274 secs] 70622K->4017K(194560K), 0.0051485 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] |
尝试在Eden区再分配5*_10MB的内存空间,内存空间不足,触发Minor GC,循环i=0分配的5*_10MB的内存空间被回收,对象b被移动到了**Survivor 0**区,内存地址变成了33170780832,GC年龄为1
循环i=2
1 | [GC (Allocation Failure) [DefNew: 57128K->3417K(92160K), 0.0075981 secs] 57580K->3869K(194560K), 0.0076253 secs] [Times: user=0.01 sys=0.01, real=0.00 secs] |
尝试在Eden区再分配5*_10MB的内存空间,内存空间不足,触发Minor GC,循环i=1分配的5*_10MB的内存空间被回收,对象b被移动到了**Survivor 1**区,内存地址变成了33160295048,GC年龄为2
循环i=3
1 | [GC (Allocation Failure) [DefNew: 56715K->3417K(92160K), 0.0020149 secs] 57168K->3869K(194560K), 0.0020339 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
尝试在Eden区再分配5*_10MB的内存空间,内存空间不足,触发Minor GC,循环i=2分配的5*_10MB的内存空间被回收,对象b**再次被移动到了Survivor 0**区,内存地址变成了33170780808,GC年龄为3
循环i=4
1 | [GC (Allocation Failure) [DefNew: 56533K->0K(92160K), 0.0052362 secs] 56985K->3870K(194560K), 0.0052590 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
尝试在Eden区再分配5*_10MB的内存空间,内存空间不足,触发Minor GC,循环i=3分配的5*_10MB的内存空间被回收,对象b的GC年龄已经达到了MaxTenuringThreshold,可以直接晋升到Tenured区,内存地址变成了33181729784,GC年龄依旧为3,不会再增加
循环i=5
1 | [GC (Allocation Failure) [DefNew: 52773K->0K(92160K), 0.0004461 secs] 56643K->3870K(194560K), 0.0004610 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] |
尝试在Eden区再分配5*_10MB的内存空间,内存空间不足,触发Minor GC,循环i=4分配的5*_10MB的内存空间被回收,对象b已经在Tenured区,此时并不是Full GC,内存地址不会改变,依旧是33181729784,GC年龄也依旧是3
动态晋升
在Minor GC时,如果在Survivor中**相同年龄的所有对象大小之和** ≧ 0.5 * sizeof(Survivor) ⇒ **大于或等于该年龄的对象直接晋升**到老年代,无须考虑MaxTenuringThreshold
代码
1 | /** |
运行结果
1 | [Full GC (System.gc()) [Tenured: 0K->484K(102400K), 0.0046554 secs] 6555K->484K(194560K), [Metaspace: 3346K->3346K(1056768K)], 0.0047017 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] |
byte[] b0 = new byte[_10MB / 8]
1 | Obj[b0] , Address:[33076281344] , GCAge:[0] |
尝试分配对象b0到**Eden**区,内存空间充足,初始内存地址为33076281344,GC年龄为0
循环i=0
1 | Obj[b0] , Address:[33076281344] , GCAge:[0] |
- 尝试在
Eden区分配5*_10MB的内存空间,内存空间充足,不需要触发Minor GC,对象b0的内存地址不变,依旧是33076281344,在**Eden**区,GC年龄为0 b1~b4尚未实例化
循环i=1
1 | [GC (Allocation Failure) [DefNew: 67248K->2254K(92160K), 0.0069139 secs] 67732K->2738K(194560K), 0.0069444 secs] [Times: user=0.00 sys=0.01, real=0.01 secs] |
- 尝试在
Eden区再分配5*_10MB的内存空间,内存空间不足,触发Minor GC,循环i=0分配的5*_10MB的内存空间被回收,对象b0被移动到了**Survivor 0**区,内存地址变成了33170752344,GC年龄为1 b1~b4尚未实例化
循环i=2
1 | [GC (Allocation Failure) [DefNew: 59673K->7226K(92160K), 0.0059408 secs] 60157K->7710K(194560K), 0.0059641 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] |
- 尝试在
Eden区再分配b1~b4的内存空间,内存空间充足,不会触发Minor GC - 尝试在
Eden区再分配5*_10MB的内存空间,内存空间不足,触发Minor GC,循环i=1分配的5*_10MB的内存空间被回收,对象b0被移动到了**Survivor 1**区,内存地址变成了33160266584,GC年龄为2 - 另外这次
Minor GC也把b1~b4移动到了**Survivor 1**区,具体信息GC日志所示
循环i=3
1 | [GC (Allocation Failure) [DefNew: 60004K->0K(92160K), 0.0086877 secs] 60488K->7710K(194560K), 0.0087102 secs] [Times: user=0.00 sys=0.01, real=0.00 secs] |
尝试在Eden区再分配5*_10MB的内存空间,内存空间不足,触发Minor GC,循环i=2分配的5*_10MB的内存空间被回收,此时**Survivor 1区中b1~b4具有相同的GC年龄,总大小 = 5M = 0.5 * sizeof(Survivor),可以进行动态晋升**,所有GC年龄大于等于1的对象都可以直接晋升到老年代,因此b0~b4直接晋升,具体信息GC日志所示
循环i=4
1 | [GC (Allocation Failure) [DefNew: 53328K->0K(92160K), 0.0009302 secs] 61039K->7710K(194560K), 0.0009761 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] |
尝试在Eden区再分配5*_10MB的内存空间,内存空间不足,触发Minor GC,循环i=3分配的5*_10MB的内存空间被回收,此时b0~b4都已经在老年代中了,此时只是Minor GC,内存地址和GC年龄都不会改变












