【JAVA核心知识】4.3: G1垃圾收集器 |
您所在的位置:网站首页 › 垃圾收集器图片 › 【JAVA核心知识】4.3: G1垃圾收集器 |
前言:
G1垃圾收集器是JAVA7引入的一款垃圾收集器,全称Garbage-First Garbage Collector。G1是一个分代的,增量的,并行与并发的标记-复制算法垃圾回收器。G1设计的目的是为了应对越来越大的内存以及越来越多的处理器数量,复制算法能很好的解决空间碎片化的问题,其分区与部分回收机制又使得它能有效减少大内存下复制算法所导致的单次GC停顿时间(清除算法可以并发处理,复制算法只能STW进行)。但是其分区特性也使得其不适合小内存场景,在小内存场景回收效率低下。 G1还有一个极其重要的特性-软实时(soft real-time):实时回收是指每一次垃圾回收所造成的停顿时间都在限定时间之内。软实时是指G1允许设置一个限定值,G1会努力控制每一次GC所造成的停顿都在限定时间之内,但是并不保证每一次GC造成的停顿都能满足要求。停顿时间的限定可以通过-XX:MaxGCPauseMillis参数设置。如果需要设置此参数,要慎重,过大的话没有意义,过小则会导致频繁触发回收,降低回收效率。 通常设到100ms、250ms之类的都可能是合理的。设到50ms就不太靠谱,G1可能一开始还跟得上,跑的时间一长就开始乱来了。 这也提醒大家:如果您的程序要长时间运行,那么在技术选型评估GC性能的时候要让测试程序跑足够长时间才能看清状况。多久才够长取决于实际应用要连续运行多久。不然一个要运行一个月才重启一次的程序,如果测试的时候只测了两个小时就觉得没问题,实际上线跑起来可能正好两个半小时的时候来了一次几分钟的full GC暂停,那就纱布了⋯ G1需要暂停来拷贝对象,而CMS在暂停中只需要扫描(mark)对象,那算法上G1的暂停时间会比CMS短么? 其实CMS在较小的堆、合适的workload的条件下暂停时间可以很轻松的短于G1。在2011年的时候Ramki告诉我堆大小的分水岭大概在10GB~15GB左右:以下的-Xmx更适合CMS,以上的才适合试用G1。现在到了2014年,G1的实现经过一定调优,大概在6GB~8GB也可以跟CMS有一比,我之前见过有在-Xmx4g的环境里G1比CMS的暂停时间更短的案例。 ------ RednaxelaFX(https://hllvm-group.iteye.com/group/topic/44381) G1垃圾收集器 前言:1. Region2. CSet3. RSet4. 年轻代收集5. 混合收集周期5.1 Previous/Next Bitmap与PTAMS/NTAMS5.2 并发标记周期5.2.1 初始标记5.2.2 根分区扫描5.2.3 并发标记5.2.4 重新标记5.2.5 清除 6. 担保机制Full GC7. 总结 1. Region不同于传统分代垃圾收集器,将堆空间固定的划分为新生代与老年代两个内存区域。G1采用分区的概念,将整个堆空间分为若干个大小相等的内存区域。每个内存区域就是一个Region(分区)。这些Region不再是固定的属于新生代或者老年代,它既可能是新生代,也可能是老年代。Region是G1的最小回收单元。 每一块Region的大小是固定的,可以通过参数XX:G1HeapRegionSize设置大小(1MB-32MB且必须是2的幂)。 使用中的Region根据其角色可以分为Eden Region, Survivor Region, Old Region以及Humongous Region。其中Eden Region,Survivor Region,Old Region顾名思义分别对应分代收集算法的中的Eden区,Survivor区,Old区,每个区都含有若干个Region。 CSet全称Collection Set。CSet中保存着每次GC中要回收的目标分区。回收分区内的存活对象会被复制到新分配的空闲分区中。因为年轻代是整体回收,所以无论是年轻代收集还是混合收集,所有的年轻代垃圾分区都会被放入CSet。老年代分区的回收只会在混合收集中涉及,在混合收集过程中,根据收益率将回收收益率较高的候选年老代分区放入CSet等待回收[注1]。 G1的分区回收是根据CSet来的,未完全死亡的分区要想被回收,首先要被放入CSet,然后在Evacuation阶段[注2],G1会针对CSet中的Region进行回收。 年轻代收集的CSet中都是年轻代Region,混合收集周期的CSet中既有年老代Region又有年轻代Region。通过CSet控制每次回收的分区数量使得G1的回收过程变的可控。 注1: 1.分区的回收收益率是该分区的垃圾占有率,垃圾占有率越高意味着收益率也就更高。因为垃圾占有率越高意味着要复制的存活对象就越少,那复制的过程也就越快,同时分区回收后获得的空闲空间也就越多。 2.并不是所有的老年分区都是候选分区,只有那些收益率达到标准的分区才会被列为候选分区,这个标准通过参数-XX:G1MixedGCLiveThresholdPercent进行设置,默认为85,意味着一个老年代分区的存活率低于85%才会被列为候选分区。 3.并不是所有的候选分区都会被放入CSet,-XX:G1OldCSetRegionThresholdPercent设置了可以放入CSet的老年代分区数量上限,默认为10,即默认最大有整个堆空间10%大小的老年代分区可以被放入CSet中(相当于10%总堆Region数)。 注2:G1的收集过程可分为两个阶段:Concurrent Marking(并发标记)与Evacuation Pauses(空间回收)。 3. RSetRSet全称Remember Set(记忆集合)。 每个Region的RSet都记录着引用当前分区对象的外部老年代分区对象所在的Region_Card[注3]。 当年轻代满[注4]时就会触发年轻代收集,回收年轻分区,并将存活次数达到晋升标准的对象移入老年代。 年轻代分区的收集是STW的,用户线程暂停,运用可达性分析法扫描所有的年轻代分区,标记GC Roote直接关联引用链中的存活对象,除此之外会扫描所有年轻代分区的RSet,标记出老年代分区引用的对象对根节点进行补充,根据完整的根对象进行引用分析就获得了年轻代分区的所有存活对象。标记完成后Eden Region中的存活对象会被复制到新的Survivor Region,而Survivor Region的存活对象满足晋升标准的晋升入老年代,不满足晋升标准的依然被复制到新的Survivor Regiong。之后释放所有的年轻代分区对内存空间进行回收。 此外,如果设置了软实时标准,G1还会根据GC的耗时进行分析,并根据设置的软实时标准调整年轻代尺寸(分区数量),以此来保证大多的年轻代收集都能满足软实时标准的要求。。 注4:年轻代大小会在初始空间-XX:G1NewSizePercent(默认整堆5%)与最大空间-XX:G1MaxNewSizePercent(默认整堆60%)之间动态变化。具体大小由G1根据软实时目标(如果设置的话),回收效率计算得到。不过G1依然允许通过参数-XX:NewRatio、-Xmn设置固定的新生代大小,但是此时软实时的特性也就失效了。 5. 混合收集周期随着程序的运行,越来越多的对象晋升入老年代,当老年代堆占比达到IHOP阈值(老年代占整堆比,可以通过-XX:InitiatingHeapOccppancyPercent设置,默认45%)时就会触发混合收集,但是混合收集不会立即执行,而是会等待下一次年轻代收集。另外为了保证停顿时间,混合收集可能只会收集老年代分区的一部分,接着就继续让应用程序运行,并在接下来的连续多次年轻代收集时触发混合收集。整个连续多次的混合收集过程被称为混合收集周期(Mixed Collection Cycle)。 通过参数-XX:G1MixedGCCountTarget控制混合周期中混合收集的最大总次数,默认为8,即整个混合收集周期会进行8次混合收集,每次至少有1/8的候选老年代分区[注5]被放入CSet中。另外通过参数-XX:G1HeapWasteParcent来设置混合收集周期的堆废物百分比,默认为5,即当老年代垃圾堆占比低于5%时,即使没达到最大总次数,混合收集周期也会停止,因为过低的垃圾堆占比会使得回收效益变的很低。 注5:目标是候选老年代分区,而不是整个堆分区。且每一次加入的分区并不是说是遍历式的,比如第一次是0/8-1/8,那下次就是1/8-2/8这样的。而是根据回收收益率来的,每次都是将回收收益率最高的那一批候选分区加入CSet。(且也不是严格的1/8,因为候选老年代分区可能在程序执行过程中继续产生。具体的数量由G1通过回收效率计算获得。) 在整个混合收集周期内,用户线程并不是处于暂停状态的,这也就意味着会不断的有新对象产生,也老对象死亡,G1也就无法实时获得整个Region的完整的存活性标记以及进行回收收益率计算。 对于这个问题,G1为每一个Region创建与其对应的位图(Bitmap)来标记对象存活情况。 每一个Region拥有两个位图,一个是Previous Bitmap,用来表示上一个混合收集过程并发标记的结果(Region的对象存活情况)。另一个是Next Bitmap,用来记录此次混合收集过程并发标记的结果(Region的对象存活情况),在此次并发标记结束时,Next Bitmap替换原有的Previous Bitmap成为下一轮回合混合收集过程的Previous Bitmap。 除了Bitmap之外,G1还通过顶部标记TAMS(top at mark start)来记录标记过的内存范围: 下图,便是一个完整的位图与顶部标记的变动周期: 混合收集过程中为了缩短停顿时间G1采用了并发标记的方式来确定对象存活性。但是G1的并发标记不同于CMS的并发标记方式。G1的并发标记过程分为:初始标记(Initial Mark),根分区扫描(Root Region Scanning),并发标记(Concurrent Mark),重新标记(Remark),清除(Cleanup)。 5.2.1 初始标记初始标记阶段与CMS类似,这个阶段需要STW,负责标记出所有的GC Roots对象。但是并不是堆空间达到IHOP阈值之后立刻就进行初始标记STW,而是会等待下一次年轻代收集,利用年轻代收集的STW完成初始标记阶段。 5.2.2 根分区扫描年轻代收集之后,年轻代存活的对象都会被放入新的Survivor Region,之后应用线程就会恢复运行。为了保证标记结果的准确性[注6],根分区扫描阶段需要将Survivor Region内的所有对象都标记为GC Roots对象。这些存活的Survivor Region也可以被称为Roots Region。此阶段不需要STW,但是这个阶段必须要在下一次年轻代收集触发之前完成,因为每次年轻代收集触发都会使得Survivor Region发生改变。 注6:Survivor区域的对象在初始标记完成阶段肯定时存活的,对于老年代分区来说,它们和初始标记时标记出来的GC Roots对象是一致的,GC Roots对象加上它们才是完整的根对象集。 5.2.3 并发标记并发标记阶段与CMS的并发标记阶段类似,对根对象进行引用链分析,标记引用链上的对象为存活。此阶段不需要STW,且此阶段可能会经历年轻代回收,这意味着分析过程中对象的引用关系可能会发生改变。但是不同于CMS通过Dirty Card来记录标记期间的改变,G1通过SATB(起始快照算法 Snapshot at the beginning)来记录标记期间引用关系的改变。因为SATB不同于DirtyCard的全堆扫描,SATB解决了CMS的主要烦恼:重新标记暂停时间长带来的潜在风险。 5.2.3.1 SATB(起始快照算法 Snapshot at the beginning) CMS的DirtyCard和G1的SATB的目的都是为了解决并发过程中的引用变动导致的漏标,避免存在有效引用的对象被当作垃圾回收掉。为了解决这个问题,首先要知道漏标是如何出现的。 并发标记阶段由于和用户线程一起执行,那么引用关系也会一直改变,G1在并发标记阶段使用SATB记录变化。重新标记阶段则会STW,使得引用不再变化,并解析SATB对对象存活性进行修正以获得完整的存活性标记。 5.2.5 清除清除阶段也是STW的,但是不同于CMS的并发清理阶段,G1的清除阶段并不会直接清理垃圾对象,而是会根据Previous Bitmap与RSet分析出回收收益,并根据回收收益进行排序,将回收收益率最高的Region放入CSet,等待下次年轻代收集触发时在Evacuation阶段对CSet中的Region进行回收[注7]。 特别的,对于无存活对象的Region,不会放入Region,而是在清除阶段直接回收。也就是说最坏的情况下(所有候选Region都还有存活对象),经历了清除阶段堆内存也不会有任何空间被释放。 清除阶段还会做另一个重要的操作:用Next Bitmap覆盖Previous Bitmap(相当于分配一个新的符合长度的Bitmap),NTAMS覆盖PTAMS,以便下一次并发标记过程使用。 注7:每次混合收集周期都是年轻代收集触发的,CSet中Region的回收也是随着年轻代收集的STW完成的,年轻代收集结束后开始并发标记, 并发标记过程将需要回收的Old Region被放入CSet,下次年代收集时年轻代的Region也会被放入CSet,然后STW时将CSet中的Old Region与年轻代Region一起回收。接着开启下一次并发标记。这就是混合收集周期。可以说混合收集=年轻代收集+并发标记。 6. 担保机制Full GC在GC过程中 ,如果无法申请到足够的空闲分区时,G1会首先尝试增加堆空间,如果扩容失败,G1就会触发担保机制:进行一次STW的Full GC,对堆空间进行清除和压缩,只保留存活对象。 导致无法申请到足够的空闲分区的原因有: 需要空闲Region时无空闲空间可用,如:年轻代新对象申请Region失败,年纪代晋升申请Region失败,回收CSet执行复制算法时申请Region失败。而造成无空闲空间可用的原因有:①混合收集过程中,因为用户线程是和GC线程并发进行的,如果混合收集回收空间的速度跟不上用户线程分配新对象的速度。使得空闲分区越来越少,最终导致无空闲空间可用。因此G1才会在老年代分区达到一定比例(IHOP阈值)时就会触发混合收集,而不是等堆空间被占满时才触发。②各个分区内部碎片化严重,虽然都有空闲空间,但是都达不到回收阈值成为候选分区,分区一直不被回收。那么就会导致空闲分区越来越少,最终导致分区回收时无空闲空间可用。堆空间较为碎片化,此时有一个超大对象需要多个连续分区,无法申请到连续分区也会导致Full GC的发生。在Java10之前,G1的担保Full GC都是单线程的。但是G1使用于多核大内存的场景,这样的话单线程无法利用多核优势,回收性能较差。因此在JAVA10,G1引入了多线程的Full GC。 7. 总结总的来说G1在大内存多核场景下有着很良好的表现,解决了大内存下一次GC因需要处理的空间更大而造成的停顿时间变长的问题。同时软实时的特性也使得GC对用户线程的影响可控。但是G1也存在着内存空间浪费的问题(Region垃圾占用率未达到回收标准时即使已识别出其内有垃圾对象,也依然不会回收),G1通过Garbage-First原则优先回收垃圾占用率最高的一批分区,使得空间浪费率处于一个可接受的范围。 PS: 【JAVA核心知识】系列导航 [持续更新中…] 上篇导航:4.2: CMS垃圾收集器 下篇导航:5: JVM的类加载 欢迎关注… 参考资料 G1垃圾收集器之RSet G1垃圾回收器详解 G1垃圾回收器详解 三色标记算法 SATB 【原创】面试官问我G1回收器怎么知道你是什么时候的垃圾? CMS与G1的最终标记 RednaxelaFX回复 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |