G1 垃圾回收的特点:三色标记法
大面积撒网、选择性捕捞
使用白灰黑三种颜色标记对象:
白色是未标记;
灰色自身被标记,引用的对象未标记;
黑色自身与引用对象都已标记。
CMS垃圾回收过程分为四个阶段:
- initial mark (初始标记)
- concurrent mark (并发标记)
- remark (重新标记)
- concurrent sweep (并发清理)
其中 并发标记 阶段会有漏标的问题,为解决这个问题,采用了 “三色标记算法”
CMS被G1取代的原因
简介
G1 GC 全称是Garbage First Garbage Collector,垃圾优先垃圾回收器,以下简称G1。G1是HotSpot JVM的短停顿垃圾回收器。G1是在2012年4月发布的JDK 7u4中才实现。从长期来说,G1旨在取代CMS(Concurrent Mark Sweep)垃圾回收器。G1从JDK9开始已经作为默认的垃圾回收器。如果对于应用程序来说停顿时间比吞吐量更重要,G1是非常合适的选择。
G1内存模型
G1相较之前其它的垃圾回收器,对模型进行了改变,不再进行物理分代,采用逻辑分代。
它不再将连续内存分为Eden区(新生代)和Old区(老年代),而是将内存分为一个个的Region。一块Region(分区)在逻辑上依然分代,分为四种:Eden(伊甸园区),Old(老年代),Survivor(幸存区),Humongous(大对象)(大对象,跨多个连续的Region)。
它的每个分区都可能是年轻代也可能是老年代,但是在同一时刻只能属于某个代。
G1还是一种带压缩的收集器,在回收老年代的分区时,是将存活的对象从一个分区拷贝到另一个可用分区,这个拷贝的过程就实现了局部的压缩。每个分区的大小从1M到32M不等,但都是2的幂次方。
特点:
- 并发收集
- 压缩空闲时间不会延长GC的暂停时间
- 更易预测GC的暂停时间
- 适用不需要实现很高吞吐量的场景
G1垃圾回收过程主要包括三个:
- 年轻代回收(young gc)过程
- 老年代并发标记(concurrent marking)过程
- 混合回收过程(mixed gc)。
应用程序分配内存,当年轻代的Eden区用尽时开始年轻代回收过程;当堆内存使用达到一定值(默认45%)时,开始老年代并发标记过程;标记完成马上开始混合回收过程。
举个例子:我曾经工作的一个Web服务器,Java进程最大堆内存为4G,每分钟响应1500个请求,每45秒钟会新分配大约2G的内存。G1会每45秒钟进行一次年轻代回收,每31个小时整个堆的使用率会达到45%,会开始老年代并发标记过程,标记完成后开始四到五次的混合回收。
年轻代回收过程(Young GC)
JVM启动时,G1先准备好Eden区,程序在运行过程中不断创建对象到Eden区,当所有的Eden区都满了,G1会启动一次年轻代垃圾回收过程。年轻代只会回收Eden区和Survivor区。首先G1停止应用程序的执行(Stop-The-World),G1创建回收集(Collection Set),回收集是指需要被回收的内存分段的集合,年轻代回收过程的回收集包含年轻代Eden区和Survivor区所有的内存分段。然后开始如下回收过程:
-
第一阶段,扫描根。
根是指static变量指向的对象,正在执行的方法调用链条上的局部变量等。根引用连同RS记录的外部引用作为扫描存活对象的入口。
-
第二阶段,更新RS。
处理dirty card queue中的card,更新RS。此阶段完成后,RS可以准确的反映老年代对所在的内存分段中对象的引用。
-
第三阶段,处理RS。
识别被老年代对象指向的Eden中的对象,这些被指向的Eden中的对象被认为是存活的对象。
-
第四阶段,复制对象。
此阶段,对象树被遍历,Eden区内存段中存活的对象会被复制到Survivor区中空的内存分段,Survivor区内存段中存活的对象如果年龄未达阈值,年龄会加1,达到阀值会被会被复制到Old区中空的内存分段。
-
第五阶段,处理引用。
处理Soft,Weak,Phantom,Final,JNI Weak 等引用。
G1老年代并发标记过程(Concurrent Marking)
当整个堆内存(包括老年代和新生代)被占满一定大小的时候(默认是45%,可以通过-XX:InitiatingHeapOccupancyPercent进行设置),老年代回收过程会被启动。具体检测堆内存使用情况的时机是年轻代回收之后或者houmongous对象分配之后。老年代回收包含标记老年代内的对象是否存活的过程,标记过程是和应用程序并发运行的(不需要Stop-The-World)。应用程序会改变指针的指向,并发执行的标记过程怎么能保证标记过程没有问题呢?并发标记过程有一种情形会对存活的对象标记不到。假设有对象A,B和C,一开始的时候B.c=C,A.c=null。当A的对象树先被扫描标记,接下来开始扫描B对象树,此时标记线程被应用程序线程抢占后停下来,应用程序把A.c=C,B.c=null。当标记线程恢复执行的时候C对象已经标记不到了,这时候C对象实际是存活的,这种情形被称作对象丢失。G1解决的方法是在对象引用被设置为空的语句(比如B.c=null)时,把原先指向的对象(C对象)保存到一个队列,代表它可能是存活的。然后会有一个重新标记(Remark)过程处理这些对象,重新标记过程是Stop-The-World的,所以可以保证标记的正确性。上述这种标记方法被称为开始时快照技术(SATB,Snapshot At The Begging)。这种方式会造成某些是垃圾的对象也被当做是存活的,所以G1会使得占用的内存被实际需要的内存大。
具体标记过程如下:
-
先进行一次年轻代回收过程,这个过程是Stop-The-World的。
老年代的回收基于年轻代的回收(比如需要年轻代回收过程对于根对象的收集,初始的存活对象的标记)。
-
- 恢复应用程序线程的执行。
-
开始老年代对象的标记过程。
此过程是与应用程序线程并发执行的。标记过程会记录弱引用情况,还会计算出每个分段的对象存活数据(比如分段内存活对象所占的百分比)。
-
- Stop-The-World。
-
重新标记(Remark)。
此阶段重新标记前面提到的STAB队列中的对象(例子中的C对象),还会处理弱引用。
-
-
回收百分之百为垃圾的内存分段。
注意:不是百分之百为垃圾的内存分段并不会被处理,这些内存分段中的垃圾是在混合回收过程(Mixed GC)中被回收的。 由于Humongous对象会独占整个内存分段,如果Humongous对象变为垃圾,则内存分段百分百为垃圾,所以会在第一时间被回收掉。
-
- 恢复应用程序线程的执行。
混合回收过程(Mixed GC)
并发标记过程结束以后,紧跟着就会开始混合回收过程。混合回收的意思是年轻代和老年代会同时被回收。并发标记结束以后,老年代中百分百为垃圾的内存分段被回收了,部分为垃圾的内存分段被计算了出来。默认情况下,这些老年代的内存分段会分8次(可以通过-XX:G1MixedGCCountTarget设置)被回收。混合回收的回收集(Collection Set)包括八分之一的老年代内存分段,Eden区内存分段,Survivor区内存分段。混合回收的算法和年轻代回收的算法完全一样,只是回收集多了老年代的内存分段。具体过程请参考上面的年轻代回收过程。
由于老年代中的内存分段默认分8次回收,G1会优先回收垃圾多的内存分段。垃圾占内存分段比例越高的,越会被先回收。并且有一个阈值会决定内存分段是否被回收,-XX:G1MixedGCLiveThresholdPercent,默认为65%,意思是垃圾占内存分段比例要达到65%才会被回收。如果垃圾占比太低,意味着存活的对象占比高,在复制的时候会花费更多的时间。
混合回收并不一定要进行8次。有一个阈值-XX:G1HeapWastePercent,默认值为10%,意思是允许整个堆内存中有10%的空间被浪费,意味着如果发现可以回收的垃圾占堆内存的比例低于10%,则不再进行混合回收。因为GC会花费很多的时间但是回收到的内存却很少。
-
Mixed GC的过程
- 1、初始标记 STW
- 2、并发标记
- 3、最终标记 STW(重新标记)
- 4、筛选回收 STW(并行)
Full GC
Full GC是指上述方式不能正常工作,G1会停止应用程序的执行(Stop-The-World),使用单线程的内存回收算法进行垃圾回收,性能会非常差,应用程序停顿时间会很长。要避免Full GC的发生,一旦发生需要进行调整。什么时候回发生Full GC呢?比如堆内存太小,当G1在复制存活对象的时候没有空的内存分段可用,则会回退到full gc,这种情况可以通过增大内存解决。
GC何时触发
-
YGC
- Eden区空间不足
- 多线程并行执行
-
FGC
- Old空间不足
- System.gc()
**注意:**G1也是存在FGC的,并且一定会被触发。当对象分配不下是会产生FGC。
如果G1产生FGC,应该做什么,如何优化?
- 扩内存
- 提高CPU性能(回收的快,业务逻辑产生对象的速度固定,垃圾回收越快,内存空间越大)
- (主要)降低MixedGC触发的阈值,让MixedGC提早发生(默认是45%)
漏标问题
在remark过程中,黑色指向了白色,如果不对黑色重新扫描,则会漏标。会把白色D对象当作没有新引用指向从而回收掉。
并发标记过程中,Mutator删除了所有从灰色到白色的引用,会产生漏标。此时白色对象应该被回收
产生漏标问题的条件有两个:
- 1.黑色对象指向了白色对象
- 2.灰色对象指向白色对象的引用消失
所以要解决漏标问题,打破两个条件之一即可:
-
跟踪黑指向白的增加
incremental update:增量更新,关注引用的增加,把黑色重新标记为灰色,下次重新扫描属性。CMS采用该方法。
-
记录灰指向白的消失
SATB snapshot at the beginning:关注引用的删除,当灰–>白消失时,要把这个 引用 推到GC的堆栈,保证白还能被GC扫描到。G1采用该方法。
为什么G1采用SATB而不用incremental update?
因为采用incremental update把黑色重新标记为灰色后,之前扫描过的还要再扫描一遍,效率太低。
G1有RSet与SATB相配合。Card Table里记录了RSet,RSet里记录了其他对象指向自己的引用,这样就不需要再扫描其他区域,只要扫描RSet就可以了。
也就是说 灰色–>白色 引用消失时,如果没有 黑色–>白色,引用会被push到堆栈,下次扫描时拿到这个引用,由于有RSet的存在,不需要扫描整个堆去查找指向白色的引用,效率比较高。SATB配合RSet浑然天成。
文章出自
- 1、2021年8月7日 - 8月8日 腾讯课堂 马士兵JVM课程
https://ke.qq.com/course/399017?taid=11917971606083241&tuin=6c381156 - 2、原文链接:https://blog.csdn.net/qq_44503377/article/details/107048994