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 Generation
Old Generation
空间不够 ➔Major GC
➔ 回收Old Generation
Method 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-World
GC
能否并发
(不是并行,是并发!)- 应用的
延迟
和吞吐量
本文认为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] |
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年龄都不会改变