JVM GC 算法、收集器、案例
1. GC 算法
1.1 引用计数
概念
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
使用者
- 微软公司的 COM 技术:Computer Object Model
- 使用 ActionScript3 的 FlashPlayer
- Python
问题
- 引用和去引用伴随加法和减法,影响性能
- 致命的缺陷:对于循环引用的对象无法进行回收
1.2 根搜索算法
由于引用计数算法的缺陷,所以JVM一般会采用一种新的算法,叫做根搜索算法。它的处理方式就是,设立若干种根对象,当任何一个根对象到某一个对象均不可达时,则认为这个对象是可被回收的。
根(GC Roots)
- 栈(栈帧中的本地变量表)中引用的对象
- 方法区中的静态成员
- 方法区中的常量引用的对象(全局变量)
- 本地方法栈中JNI(一般说的Native方法)引用的对象
可达性分析
什么叫不可达?
简单来说,从根(GC Roots)的对象作为起始点,开始向下搜索,搜索所走过的路径称为“引用链”,当一个对象到GC Roots没有任何引用链相连(用图论的概念来讲,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
在根搜索算法的基础上,现代虚拟机的实现当中,垃圾搜集的算法主要有三种,分别是标记-清除算法、复制算法、标记-整理算法。这三种算法都扩充了根搜索算法,不过它们理解起来还是非常好理解的。
标记清除
标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被成为stop the world),然后进行这两项工作
1. 标记:标记的过程其实就是,遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。
2. 清除:清除的过程将遍历堆中所有的对象,将没有标记的对象全部清除掉。
缺点
- 效率比较低(递归与全堆对象遍历),导致stop the world的时间比较长,追求稳定低时延的应用不建议
- 这种方式清理出来的空闲内存是不连续的,我们的死亡对象都是随即的出现在内存的各个角落的,现在把它们清除之后,内存的布局自然会乱七八糟。而为了应付这一点,JVM就不得不维持一个内存的空闲列表,这又是一种开销。而且在分配数组对象的时候,寻找连续的内存空间会不太好找。
复制(新生代的GC)
概念
将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。
特点
- 回收效率高于标记-清除算法
- 不适用于存活对象较多的场合,如老年代(复制算法适合做新生代的GC),因为老年代对象通常生命周期较长,需要多次拷贝
原理
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是说,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的空间会被浪费。
当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖于老年代进行分配担保,所以大对象直接进入老年代。整个过程如下图所示:
标记压缩/整理(老年代的GC)
概念
标记-压缩算法适合用于存活对象较多的场合,如老年代。它在标记-清除算法的基础上做了一些优化。和标记-清除算法一样,标记-压缩算法也首先需要从根节点开始,对所有可达对象做一次标记;但之后,它并不简单的清理未标记的对象,而是将所有的存活对象压缩到内存的一端;之后,一次性清理边界外所有的空间。
1. 标记:它的第一个阶段与标记/清除算法是一模一样的,均是遍历GC Roots,然后将存活的对象标记。
2. 整理:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。因此,第二阶段才称为整理阶段。
标记整理算法不仅可以弥补标记清除算法当中,内存区域分散、清除速度慢的缺点,也消除了复制算法当中,内存减半的高额代价
缺点
- 标记/整理算法唯一的缺点就是效率也不算太高,不仅要标记所有存活对象,还要整理所有存活对象的引用地址。
三种算法总结
三个算法都基于根搜索算法去判断一个对象是否应该被回收,在GC线程开启时,或者说GC过程开始时,它们都要暂停应用程序(stop the world)。
它们的区别如下:
- 效率:复制算法>标记整理算法>标记清除算法(此处的效率只是简单的对比时间复杂度,实际情况不一定如此)。
- 内存整齐度:复制算法=标记整理算法>标记清除算法。
- 内存利用率:标记整理算法=标记清除算法>复制算法。
- 可以看到标记清除算法是比较落后的算法了,但是后两种算法却是在此基础上建立的。
- 时间与空间不可兼得。
1.3 分代收集算法
当前商业虚拟机的GC都是采用的“分代收集算法”,这并不是什么新的思想,只是根据对象的存活周期的不同将内存划分为几块儿。一般是把Java堆分为新生代和老年代:短命对象归为新生代,长命对象归为老年代。
- 新生代:少量对象存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。
- 老年代:大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行GC。
注:老年代的对象中,有一小部分是因为在新生代回收时,老年代做担保,进来的对象;绝大部分对象是因为很多次GC都没有被回收掉而进入老年代。
1.4 思考题
疑问一:为什么非要停止程序的运行呢?
这通常被称之为Stop-The-World,这是Java中一种全局暂停的现象,全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互,多半情况下是由于GC引起。少数情况下由其他情况下引起,如:Dump线程、死锁检查、堆Dump。
至于GC时为什么会有全局停顿?主要以下方面考虑。
为了保证能彻底清理干净,GC的工作必须在一个能确保一致性类似快照中进行。
这里的一致性的意思是:在整个分析期间整个执行系统看起来就像被冻结在某个时间点上,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性无法得到保证。
假若GC线程和工作线程并行,新对象可能错过标记阶段导致没有被打上标记,则在清理阶段会被GC线程清理掉。
2. GC 回收器
2.1 GC 回收期分类
- 串行收集器 Serial:Serial、Serial Old
- 并行收集器 Parallel:Parallel Scavenge、Parallel Old,吞吐量优先
- 并发收集器 Concurrent:CMS、G1
2.2 并行、并发、停顿时间、吞吐量
并行(Parallel):并非工作线程和GC线程并行,而是工作线程等待,GC多线程并行工作,适合科学技术、实时性要求不高的场景
并发(Concurrent):工作线程和GC线程同时运行(但不一定是并行的,可能会交替执行),GC线程在执行时不会停顿用户线程,适合对实时性要求高的场景
停顿时间: GC收集器做GC时中断应用执行的时间,相关参数:-XX:MaxGCPauseMillis
吞吐量: 花在垃圾收集的时间和花在应用时间的占比,相关参数:-XX:GCTimeRatio=<n>
,垃圾收集时间占: 1/1+n
2.3 串行收集器
参数配置
开启配置
-XX:+UseSerialGC -XX:+UseSerialOldGC
日志格式
...
2.4 并行收集器(> 2G & 2 core 默认)
参数配置
自适应:生产环境并不常用
PARALLEL_OPTION="-XX:+UseParallelGC -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=200 -XX:GCTimeRatio=99 -Xms=512M -XX:YoungGenerationSizeIncrement=<默认年轻代增长因子为20%> -XX:TenuredGenerationSizeIncrement=<默认老年代增长因子为20%> -XX:AdaptiveSizeDecrementScaleFactor=<默认缩容因子4%>"
日志格式
版本一:简版
GC参数
GC_LOG_OPTION=" -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStampp
s -Xloggc:$CATALINA_HOME/logs/gc.log -XX:+PrintTenuringDistrii
bution "
JAVA_OPTS="$JAVA_OPTS $GC_LOG_OPTION -XX:+UseParallelGC $JSSE_OPTS"
日志内容
OpenJDK 64-Bit Server VM (25.222-b05) for linux-amd64 JRE (1.8.0_222-ea-8u222-b05-1~14.04-b05), built on Jun 28 2019 14:10:04 by "buildd" with gcc 4.8.4
Memory: 4k page, physical 1918932k(201580k free), swap 0k(0k free)
CommandLine flags: -XX:InitialHeapSize=30702912 -XX:+ManagementServer -XX:MaxHeapSize=491246592 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
2019-07-16T11:19:19.862+0800: 0.819: [GC (Allocation Failure)
Desired survivor size 1048576 bytes, new threshold 7 (max 15)
[PSYoungGen: 8192K->1008K(9216K)] 8192K->2384K(29696K), 0.0065970 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2019-07-16T11:19:20.353+0800: 1.309: [GC (Allocation Failure)
Desired survivor size 1048576 bytes, new threshold 7 (max 15)
[PSYoungGen: 9200K->1008K(9216K)] 10576K->3854K(29696K), 0.0091767 secs] [Times: user=0.00 sys=0.01, real=0.01 secs]
2019-07-16T11:19:20.822+0800: 1.779: [GC (Allocation Failure)
Desired survivor size 1048576 bytes, new threshold 7 (max 15)
[PSYoungGen: 9200K->1008K(9216K)] 12046K->5889K(29696K), 0.0075970 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2019-07-16T11:19:21.124+0800: 2.081: [GC (Allocation Failure)
Desired survivor size 1048576 bytes, new threshold 7 (max 15)
[PSYoungGen: 9200K->1008K(17408K)] 14081K->7339K(37888K), 0.0087899 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2019-07-16T11:19:21.636+0800: 2.593: [GC (Allocation Failure)
Desired survivor size 3670016 bytes, new threshold 6 (max 15)
[PSYoungGen: 17392K->1024K(17408K)] 23723K->9704K(37888K), 0.0146408 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2019-07-16T11:20:48.101+0800: 89.057: [GC (Allocation Failure)
Desired survivor size 4194304 bytes, new threshold 5 (max 15)
[PSYoungGen: 17408K->3572K(35840K)] 26088K->12301K(56320K), 0.0108407 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
格式:(GC原因)[GC分区类型TGC|OGC|FGC:GC前分区占用->GC后分区大小(分区总大小) GC前堆占用->GC后堆占用(整堆大小), GC耗时]
版本二:这是自己容器产生的GC日志
GC参数
GC_LOG_OPTION=" -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStampp
s -Xloggc:$CATALINA_HOME/logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintTenuringDistrii
bution "
JAVA_OPTS="$JAVA_OPTS $GC_LOG_OPTION -XX:+UseParallelGC $JSSE_OPTS"
日志内容如下
# JDK 版本信息
OpenJDK 64-Bit Server VM (25.222-b05) for linux-amd64 JRE (1.8.0_222-ea-8u222-b05-1~14.04-b05), built on Jun 28 2019 14:10:04 by "buildd" with gcc 4.8.4
# 节点内存信息
Memory: 4k page, physical 1918932k(174924k free), swap 0k(0k free)
# JVM 参数
CommandLine flags: -XX:InitialHeapSize=30702912 -XX:+ManagementServer -XX:MaxHeapSize=491246592 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintHeapAtGC -XX:+PrintTenuringDistribution -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
{Heap before GC invocations=1 (full 0):
# YGC、总大小 9216K、已占用 8192K
PSYoungGen total 9216K, used 8192K [0x00000000f6380000, 0x00000000f6d80000, 0x0000000100000000)
# Eden 使用率,由此可得知YGC原因在于Eden满了
eden space 8192K, 100% used [0x00000000f6380000,0x00000000f6b80000,0x00000000f6b80000)
# From、To 暂未使用,因为是第一次发生YGC
from space 1024K, 0% used [0x00000000f6c80000,0x00000000f6c80000,0x00000000f6d80000)
to space 1024K, 0% used [0x00000000f6b80000,0x00000000f6b80000,0x00000000f6c80000)
# 年老代当前为空
ParOldGen total 20480K, used 0K [0x00000000e2a00000, 0x00000000e3e00000, 0x00000000f6380000)
object space 20480K, 0% used [0x00000000e2a00000,0x00000000e2a00000,0x00000000e3e00000)
# 元空间使用情况
Metaspace used 9609K, capacity 9854K, committed 9984K, reserved 1058816K
class space used 1102K, capacity 1205K, committed 1280K, reserved 1048576K
# GC 事件,事件、原因、类型
2019-07-16T10:43:22.656+0800: 0.843: [GC (Allocation Failure)
Desired survivor size 1048576 bytes, new threshold 7 (max 15)
# PSYoungGen 表明此次GC为YGC,回收前年轻代为8192K,回收后为1008K,年轻代总大小为9216K,整理前堆占用 -> 整理后堆占用 整堆大小 耗费时间
[PSYoungGen: 8192K->1008K(9216K)] 8192K->2376K(29696K), 0.0258088 secs] [Times: user=0.01 sys=0.00, real=0.03 secs]
Heap after GC invocations=1 (full 0):
PSYoungGen total 9216K, used 1008K [0x00000000f6380000, 0x00000000f7580000, 0x0000000100000000)
eden space 8192K, 0% used [0x00000000f6380000,0x00000000f6380000,0x00000000f6b80000)
# 这里可以看到之前Eden的1008k的对象复制到from中了,所以占用率 上去了
from space 1024K, 98% used [0x00000000f6b80000,0x00000000f6c7c010,0x00000000f6c80000)
to space 1024K, 0% used [0x00000000f7480000,0x00000000f7480000,0x00000000f7580000)
# 老年代也开始有对象进入了
ParOldGen total 20480K, used 1368K [0x00000000e2a00000, 0x00000000e3e00000, 0x00000000f6380000)
object space 20480K, 6% used [0x00000000e2a00000,0x00000000e2b56388,0x00000000e3e00000)
Metaspace used 9609K, capacity 9854K, committed 9984K, reserved 1058816K
class space used 1102K, capacity 1205K, committed 1280K, reserved 1048576K
}
{Heap before GC invocations=2 (full 0):
PSYoungGen total 9216K, used 9200K [0x00000000f6380000, 0x00000000f7580000, 0x0000000100000000)
eden space 8192K, 100% used [0x00000000f6380000,0x00000000f6b80000,0x00000000f6b80000)
from space 1024K, 98% used [0x00000000f6b80000,0x00000000f6c7c010,0x00000000f6c80000)
to space 1024K, 0% used [0x00000000f7480000,0x00000000f7480000,0x00000000f7580000)
ParOldGen total 20480K, used 1368K [0x00000000e2a00000, 0x00000000e3e00000, 0x00000000f6380000)
object space 20480K, 6% used [0x00000000e2a00000,0x00000000e2b56388,0x00000000e3e00000)
Metaspace used 11545K, capacity 11862K, committed 11904K, reserved 1060864K
class space used 1286K, capacity 1385K, committed 1408K, reserved 1048576K
2.5 并发收集器
2.5.1 CMS
工作原理
特点是低停顿、低延迟收集器、并发收集
CMS标记清理6步骤
- CMS Initial Mark:初始标记Root,STW
- CMS Concurrent mark:并发标记
- CMS-concurrent-preclean:并发预清理
- CMS remark:重新标记,STW
- CMS Concurrent sweep:并发清除
- CMS-Concurrent-reset:并发重置
缺点:
- CPU 敏感,应用占用CPU对GC造成影响
- 浮动垃圾,清理过程中,内存分配
- 空间碎片
参数配置
CMS_OPTION="-XX:+UseConcMarkSweepGC -XX:ConcurrentThreads=3 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5 -XX:CMSInitiatingOccupancyFraction=<>
-XX:+CMSScavengeBeforeRemark -XX:+CMSClassUnloadingEnabled "
- -XX:+UseConcMarkSweepGC: 开启CMS收集器
- -XX:ConcurrentThreads=4: GC并发线程为4
- **-XX:+UseCMSCompactAtFullCollection: ** FullGC后压缩整理,解决内存碎片问题
- XX:CMSFullGCsBeforeCompaction: 几次FGC才压缩整理
- **XX:CMSInitiatingOccupancyFraction: ** 年老代使用率到达多少才FGC,默认92%触发FGC
- **-XX:+CMSScavengeBeforeRemark: ** FGC前是否执行下YGC
- **-XX:+CMSClassUnloadingEnabled: ** 启用回收Perm区
日志格式
GC参数
GC_LOG_OPTION=" -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStampp
s -Xloggc:$CATALINA_HOME/logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintTenuringDistrii
bution "
JAVA_OPTS="$JAVA_OPTS $GC_LOG_OPTION -XX:+UseConcMarkSweepGC $JSSE_OPTS"
日志内容如下
# 第一阶段:CMS Initial Mark:初始标记Root,STW
# 格式:Old区占用(Old区总大小) 已占用堆大小(总堆大小Eden + survivor + Old)
2019-07-16T16:07:50.027+0800: 103.489: [GC (CMS Initial Mark) [1 CMS-initial-mark: 10860K(20480K)] 12636K(29696K), 0.0022238 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2019-07-16T16:07:50.029+0800: 103.492: [CMS-concurrent-mark-start]
# 第二阶段:并发标记
2019-07-16T16:07:50.105+0800: 103.567: [CMS-concurrent-mark: 0.076/0.076 secs] [Times: user=0.07 sys=0.00, real=0.07 secs]
# 第三阶段:预清理
2019-07-16T16:07:50.105+0800: 103.567: [CMS-concurrent-preclean-start]
2019-07-16T16:07:50.106+0800: 103.568: [CMS-concurrent-preclean: 0.001/0.001 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
# 第四阶段:最终标记
2019-07-16T16:07:50.109+0800: 103.572: [GC (CMS Final Remark) [YG occupancy: 2231 K (9216 K)]2019-07-16T16:07:50.109+0800: 103.572: [Rescan (parallel) , 0.0026996 secs]
# 处理弱引用、卸载类
2019-07-16T16:07:50.112+0800: 103.575: [weak refs processing, 0.0001239 secs]2019-07-16T16:07:50.112+0800: 103.575: [class unloading, 0.0049503 secs]
# 擦除符号表
2019-07-16T16:07:50.117+0800: 103.580: [scrub symbol table, 0.0065086 secs]
# 擦除字符串表 STW
2019-07-16T16:07:50.124+0800: 103.586: [scrub string table, 0.0045382 secs][1 CMS-remark: 10860K(20480K)] 13091K(29696K), 0.0190326 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
# 第五阶段:CMS Concurrent sweep:并发清除
2019-07-16T16:07:50.136+0800: 103.598: [CMS-concurrent-sweep-start]
2019-07-16T16:07:50.144+0800: 103.607: [CMS-concurrent-sweep: 0.008/0.008 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2019-07-16T16:07:50.145+0800: 103.608: [CMS-concurrent-reset-start]
2019-07-16T16:07:50.156+0800: 103.618: [CMS-concurrent-reset: 0.010/0.010 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2019-07-16T16:07:50.144+0800: 103.607: [CMS-concurrent-sweep: 0.008/0.008 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2019-07-16T16:07:50.145+0800: 103.608: [CMS-concurrent-reset-start]
# 第六阶段:CMS-Concurrent-reset:并发重置
2019-07-16T16:07:50.156+0800: 103.618: [CMS-concurrent-reset: 0.010/0.010 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
2.5.2 G1
G1 收集器概述
从JDK(1.3)开始,HotSpot团队一直努力朝着高效收集、减少停顿(STW: Stop The World)的方向努力,也贡献了从串行Serial收集器、到并行收集器Parallerl收集器,再到CMS并发收集器,乃至如今的G1在内的一系列优秀的垃圾收集器。
G1是当今GC技术最前沿的成果之一,JDK 7中加入,G1侧重低时延,同时适合大堆内存的GC,官方也推荐G1替代CMS。
G1 收集器特点
- G1最大的特点是引入分区的思路,弱化分代概念。
- 合理利用垃圾收集各个周期的资源,解决了其他收集器甚至CMS的众多缺陷
G1 相比较CMS的改进
- 算法: G1基于标记-整理算法, 不会产生空间碎片,分配大对象时不会无法得到连续的空间而提前触发一次FULL GC。
- 停顿时间: 停顿时间可控,G1可以通过设置预期停顿时间(Pause Time)来控制垃圾收集时间避免应用雪崩现象。
- 并行、并发:G1能更充分的利用CPU,多核环境下的硬件优势来缩短stop the world的停顿时间。
CMS和G1的区别
- CMS中以分代区域为收集单位,在G1中,堆被平均分成几个区域(region),在每个区域中,虽然也保留了新老代的概念,但收集器以整个区域为单位GC。
- G1在回收内存后会马上同时做合并空闲内存的工作、而CMS默认是在STW(stop the world)的时候做。
- G1会在Young GC中使用、而CMS只能在O区使用。
工作原理
G1堆内存结构
堆内存会被切分成为很多个固定大小区域(Region),每个是连续范围的虚拟内存。
堆内存中一个区域(Region)的大小可以通过-XX:G1HeapRegionSize参数指定,大小区间最小1M、最大32M,总之是2的幂次方。
G1堆内存分配
每个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden,Survivor和老年代。
存活的对象从一个区域转移(即复制或移动)到另一个区域。区域被设计为并行收集垃圾,每个Region被标记了E、S、O和H,这些区域在逻辑上被映射为Eden,Survivor和老年代。
存活的对象从一个区域转移(即复制或移动)到另一个区域。区域被设计为并行收集垃圾,可能会暂停所有应用线程。
如上图所示,区域可以分配到Eden,survivor和老年代。此外,还有第四种类型,被称为巨型区域(Humongous Region)。Humongous区域是为了那些存储超过50%标准region大小的对象而设计的,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
如上图所示,区域可以分配到Eden,survivor和老年代。此外,还有第四种类型,被称为巨型区域(Humongous Region)。Humongous区域是为了那些存储超过50%标准region大小的对象而设计的,它用来专门存放巨型对象。如果一个H区装不下一个巨型对象,那么G1会寻找连续的H分区来存储。为了能找到连续的H区,有时候不得不启动Full GC。
G1回收流程
- **初始标记(Initial Marking )**:这个阶段是STW(Stop the World )的,所有应用线程会被暂停,标记出从GC Root开始直接可达的对象。
- **并发标记(Concurrent mark)**:从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长。
- **最终标记(Final Marking)**:标记那些在并发标记阶段发生变化的对象,将被回收。
- 筛选回收: 首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间制定回收计划,回收一部分Region。
G1垃圾回收模式
G1中提供了两种模式垃圾回收模式,Young GC和Mixed GC,两种都是Stop The World(STW)的。
- YoungGC 年轻代收集:在分配一般对象(非巨型对象)时,当所有eden region使用达到最大阀值并且无法申请足够内存时,会触发一次YoungGC。每次younggc会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。
- mixed gc:当越来越多的对象晋升到老年代old region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器,即mixed gc,该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。
补充说明:G1没有fullGC概念,需要fullGC时,调用serialOldGC进行全堆扫描(包括eden、survivor、o、perm)。
参数配置
-XX:+UseG1
日志格式
GC参数
GC_LOG_OPTION=" -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStampp
s -Xloggc:$CATALINA_HOME/logs/gc.log -XX:+PrintHeapAtGC -XX:+PrintTenuringDistrii
bution "
JAVA_OPTS="$JAVA_OPTS $GC_LOG_OPTION -XX:+UseG1GC $JSSE_OPTS"
GC 日志输出
# 老样子版本信心
OpenJDK 64-Bit Server VM (25.222-b05) for linux-amd64 JRE (1.8.0_222-ea-8u222-b05-1~14.04-b05), built on Jun 28 2019 14:10:04 by "buildd" with gcc 4.8.4
# 节点内存信息
Memory: 4k page, physical 1918932k(178628k free), swap 0k(0k free)
# JVM 参数
CommandLine flags: -XX:InitialHeapSize=30702912 -XX:+ManagementServer -XX:MaxHeapSize=491246592 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintTenuringDistribution -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC
# GC 事件 发生了YGC 此时产生了STW
2019-07-16T16:37:34.211+0800: 0.776: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 524288 bytes, new threshold 15 (max 15)
, 0.0079783 secs]
# GC 线程数量是1,因为节点只有1核,默认是跟着核心走
[Parallel Time: 7.7 ms, GC Workers: 1]
# GC 线程开始工作时间戳
[GC Worker Start (ms): 775.9]
# 相关工作消耗时间
[Ext Root Scanning (ms): 2.9]
[Update RS (ms): 0.0]
[Processed Buffers: 0]
[Scan RS (ms): 0.0]
[Code Root Scanning (ms): 0.2]
[Object Copy (ms): 4.4]
[Termination (ms): 0.0]
[Termination Attempts: 1]
[GC Worker Other (ms): 0.0]
[GC Worker Total (ms): 7.5]
[GC Worker End (ms): 783.4]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.3 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.2 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
# GC 回收空间情况 eden区当前使用(Eden总大小) -> Eden回收后占用(Eden发送缩容后总大小) Survivor区GC前占用->YGC后占用 GC前堆占用(整堆大小)->GC后堆占用(整堆大小)
[Eden: 3072.0K(3072.0K)->0.0B(1024.0K) Survivors: 0.0B->1024.0K Heap: 3072.0K(30720.0K)->1396.0K(30720.0K)]
[Times: user=0.00 sys=0.00, real=0.01 secs]
...
...
...
# YGC回收后堆的占用率,24576.0K / (30720.0K) * 100 = 80 > InitiatingHeapOccupancyPercent=5 百分之5
[Eden: 13312.0K(13312.0K)->0.0B(11264.0K) Survivors: 3072.0K->2048.0K Heap: 24576.0K(30720.0K)->13824.0K(30720.0K)]
[Times: user=0.02 sys=0.00, real=0.01 secs]
# 大于InitiatingHeapOccupancyPercent会触发全局并发标记 Global Concurrent Marking
# 开始扫描根扫描初始标记,这个阶段是STW(Stop the World )的,所有应用线程会被暂停,标记出从GC Root开始直接可达的对象
2019-07-16T17:01:54.688+0800: 18.708: [GC concurrent-root-region-scan-start]
2019-07-16T17:01:54.694+0800: 18.715: [GC concurrent-root-region-scan-end, 0.0061383 secs]
# 2. 开始并发标记(Concurrent mark),从GC Roots开始对堆中对象进行可达性分析,找出存活对象,耗时较长。
2019-07-16T17:01:54.694+0800: 18.715: [GC concurrent-mark-start]
2019-07-16T17:01:54.717+0800: 18.738: [GC concurrent-mark-end, 0.0234630 secs]
# 3.最终标记(Final Marking),标记那些在并发标记阶段发生变化的对象,将被回收
2019-07-16T17:01:54.717+0800: 18.738: [GC remark 2019-07-16T17:01:54.717+0800: 18.738: [Finalize Marking, 0.0000707 secs] 2019-07-16T17:01:54.718+0800: 18.738: [GC ref-proc, 0.0001169 secs] 2019-07-16T17:01:54.718+0800: 18.738: [Unloading, 0.0089852 secs], 0.0093950 secs]
[Times: user=0.01 sys=0.00, real=0.00 secs]
2019-07-16T17:01:54.727+0800: 18.748: [GC cleanup 14121K->11049K(30720K), 0.0001833 secs]
[Times: user=0.00 sys=0.00, real=0.00 secs]
# 4. 筛选回收,首先对各个Regin的回收价值和成本进行排序,根据用户所期待的GC停顿时间制定回收计划,回收一部分Region
2019-07-16T17:01:54.727+0800: 18.748: [GC concurrent-cleanup-start]
2019-07-16T17:01:54.727+0800: 18.748: [GC concurrent-cleanup-end, 0.0000156 secs]
2019-07-16T17:02:46.763+0800: 70.784: [GC pause (G1 Evacuation Pause) (young)
Desired survivor size 1048576 bytes, new threshold 15 (max 15)
- age 1: 715040 bytes, 715040 total
, 0.0166524 secs]
[Parallel Time: 15.6 ms, GC Workers: 1]
[GC Worker Start (ms): 70783.7]
[Ext Root Scanning (ms): 2.5]
[Update RS (ms): 6.2]
[Processed Buffers: 16]
[Scan RS (ms): 0.1]
[Code Root Scanning (ms): 0.3]
[Object Copy (ms): 6.5]
[Termination (ms): 0.0]
[Termination Attempts: 1]
[GC Worker Other (ms): 0.0]
[GC Worker Total (ms): 15.6]
[GC Worker End (ms): 70799.3]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 1.0 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.8 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 11264.0K(11264.0K)->0.0B(1024.0K) Survivors: 2048.0K->2048.0K Heap: 22016.0K(30720.0K)->11264.0K(30720.0K)]
[Times: user=0.01 sys=0.00, real=0.02 secs]
# 当全局并发标记完成后,此时JVM可知道有多少空间可被回收,如果垃圾占比超过G1HeapWastePercent该百分比值,则触发Mixed GC
# mixed gc 该算法并不是一个old gc,除了回收整个young region,还会回收一部分的old region,这里需要注意:是一部分老年代,而不是全部老年代,可以选择哪些old region进行收集,从而可以对垃圾回收的耗时时间进行控制。
2019-07-16T17:02:46.850+0800: 70.871: [GC pause (G1 Evacuation Pause) (mixed)
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age 1: 680384 bytes, 680384 total
- age 2: 614624 bytes, 1295008 total
, 0.0104075 secs]
[Parallel Time: 10.2 ms, GC Workers: 1]
[GC Worker Start (ms): 70871.1]
[Ext Root Scanning (ms): 2.6]
[Update RS (ms): 1.2]
[Processed Buffers: 9]
[Scan RS (ms): 0.2]
[Code Root Scanning (ms): 0.2]
[Object Copy (ms): 6.0]
[Termination (ms): 0.0]
[Termination Attempts: 1]
[GC Worker Other (ms): 0.0]
[GC Worker Total (ms): 10.2]
[GC Worker End (ms): 70881.3]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 1024.0K(1024.0K)->0.0B(15360.0K) Survivors: 2048.0K->1024.0K Heap: 12288.0K(30720.0K)->10240.0K(30720.0K)]
[Times: user=0.01 sys=0.00, real=0.01 secs]
...
...
2019-07-16T17:02:46.850+0800: 70.871: [GC pause (G1 Evacuation Pause) (mixed)
Desired survivor size 524288 bytes, new threshold 1 (max 15)
- age 1: 680384 bytes, 680384 total
- age 2: 614624 bytes, 1295008 total
, 0.0104075 secs]
[Parallel Time: 10.2 ms, GC Workers: 1]
[GC Worker Start (ms): 70871.1]
[Ext Root Scanning (ms): 2.6]
[Update RS (ms): 1.2]
[Processed Buffers: 9]
[Scan RS (ms): 0.2]
[Code Root Scanning (ms): 0.2]
[Object Copy (ms): 6.0]
[Termination (ms): 0.0]
[Termination Attempts: 1]
[GC Worker Other (ms): 0.0]
[GC Worker Total (ms): 10.2]
[GC Worker End (ms): 70881.3]
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.0 ms]
[Other: 0.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 0.1 ms]
[Ref Enq: 0.0 ms]
[Redirty Cards: 0.0 ms]
[Humongous Register: 0.0 ms]
[Humongous Reclaim: 0.0 ms]
[Free CSet: 0.0 ms]
[Eden: 1024.0K(1024.0K)->0.0B(15360.0K) Survivors: 2048.0K->1024.0K Heap: 12288.0K(30720.0K)->10240.0K(30720.0K)]
[Times: user=0.01 sys=0.00, real=0.01 secs]
应用场景
G1 的第一个重要特点是为用户的应用程序的提供一个低GC延时和大内存GC的解决方案。这意味着堆大小6GB或更大,稳定和可预测的暂停时间将低于0.5秒。
就目前而言、CMS还是默认首选的GC策略、但是可能在以下场景下G1更适合:
- 服务端多核CPU、JVM内存占用较大的应用(至少大于4G)
- 应用在运行过程中会产生大量内存碎片、需要经常压缩空间
- 想要更可控、可预期的GC停顿周期,防止高并发下应用雪崩现象
如果应用程序使用 CMS 或 ParallelOld 垃圾回收器具有一个或多个以下特征,将有利于切换到 G1:
- Full GC 持续时间太长或太频繁
- 对象分配率或年轻代升级老年代很频繁
- 不期望的很长的垃圾收集时间或压缩暂停(超过0.5至1秒)
注意:如果你正在使用 CMS 或 ParallelOld 收集器,并且你的应用程序没有遇到长时间的垃圾收集暂停,则保持与您的当前收集器是很好的,升级 JDK 并不必要更新收集器为G1
2.6 选择垃圾收集器
官网指导原则:
- 小于100M、单核、停顿时间不敏感,串行垃圾收集器
- 允许停顿时间>1s,并行收集器
- 不允许超过1s,并发收集器
思考题
1. 为什么G1 回收器比前几个应用更广泛?
答案如下
- 内存设计更现代化,适合当下大内存以及对低延迟的需求
- 用户灵活度更好,可以控制STW对应用的影响
- G1 可以为多年龄代进行 GC