当前位置: 首页 > 科技观察

G1Garbagecollector

时间:2023-03-21 13:13:35 科技观察

forserver-side(multi-CPU)applications通用规则:首先收集尽可能多的垃圾(GarbageFirst)一定程度上可以理解为对CMS的改进,没有全局分区。G1在内存耗尽(串行、并行)或快耗尽(CMS)时不启动垃圾回收,而是在内部使用启发式算法在老年代中寻找回收回报率高的分区进行回收。特点:并发和并行:G1可以在多核环境下充分利用多CPU和硬件优势,利用多CPU缩短STW停顿时间。有些收集器需要停止Java线程来执行GC动作,而G1收集器仍然可以让Java程序以并发的方式继续运行。分代收集:G1可以自己管理整个Java堆,只在逻辑上区分新生代和老年代。对新创建的对象、存活一段时间的对象、经历多次GC存活的旧对象采用不同的处理方式,以达到更好的回收效果。空间整合:G1在运行过程中不会产生空间锁,回收后可以提供规律可用的内存。G1将内存划分为大小相等的内存分区。回收时,以分区为单位进行回收,将存活的对象复制到另一个空闲分区。由于所有的操作都是以等大小的分区为单位进行的,所以G1自然是一种压缩方案(部分压缩);可预测的停顿:除了追求低停顿,G1还可以建立一个可以预测停顿时间的模型。它允许用户明确指定在M毫秒的时间段内,垃圾回收所花费的时间不应超过N毫秒。新生代和总堆的大小可以根据用户设置的停顿时间目标自动调整。暂停目标越短,新生代空间越小,总空间越大;G1模型内存模型区域(Region)G1采用了内存分区的概念。将整个堆分成几个大小相等的区域,逐步使用;G1只要求对象在逻辑上是连续的。区域不会绑定代数,代数可以切换。整个堆的大小(1-32mb,2^n)可以通过-XX:G1HeapRegionSize=n来设置,默认整个堆分为2048个分区。说白了就是-Xms/2048。如果小于1,则值为1,如果大于32,则值为32;其他值类似2、4、8、16。Card(卡片)每组分成多张卡片,大小为512byte。同时,G1GC为每个区间设置一个全局内存块表(GlobalCardTable),帮助维护所有的堆内存块。内存回收是在卡上完成的。堆(head)代表了整个空间的总大小。可以用-Xms/-Xmx指定。当发生年轻代收集或混合收集时,通过计算GC和申请的耗时比自动调整堆空间。GC频率越高,堆空间越大,GC使用时间越长,堆空间越小。GC时间与应用程序时间的比率默认为9。当空间不足时,它会先尝试扩容,如果失败则进行FullGC。分代模型分代更关注最近分配的对象,避免对生命周期长的对象进行更改。借鉴分代的思想,将内存区在逻辑上分为新生代、幸存者代和老年代。但是JVM会动态调整空闲空间给新生代空间。新生代会在初始空间(-XX:G1NewSizePercent默认为5%)和最大空间(-XX:G1MaxNewSizePercent默认为60%)之间动态变化,由参数目标暂停时间(-XX:MaxGCPauseMillis)在本地分配默认为200ms)Localallocationbuffer(Lab)是一个分区的内存,所以每个线程都可以接收一部分内存来使用。这样接收内存和GC任务的内存是独立进行的,从而减少了同步时间,提高了GC的效率。这个线程被调用后的分区称为本地分配缓存。分区模型G1以“Region”为单位分配内存,以“Card”为单位分配区域内的“对象”。HumongousRegion(巨大区域)大于分区大小一半的对象称为HumongousObjects。线程不会直接在TLAB中创建对象,因为大对象的移动成本很高,甚至分区也无法容纳它们。对于巨大的对象,会直接分配到老年代的连续空间,占用的连续空间称为“HumongousRegion”。优化:如果巨型对象没有指向巨型对象,会在新生代回收周期直接回收。巨大的对象将独占一个或多个物理连续分区。第一个分区标记为StartsHumongous,相邻的连续分区标记为ContinuesHumongous。由于不能享受Lab带来的优化,需要扫描整个堆来确定一块连续的内存空间,确定大对象起始位置的成本非常高。如果可能,应用程序应避免生成巨大的对象。RememberedsetRememberSet(RSet)串行和并行GC扫描整个堆以确认可达性。G1为每个分区建立一个记忆集(RSet),记录引用分区中对象的卡片索引(反向索引,谁引用了我)。当分区要被回收时,它会扫描分区的RSet,判断分区中的对象是否存活,进而判断该分区中的对象是否存活。RSet为空,说明这个Region中的对象没有被其他对象引用。两种场景都依赖Rset加速(因为新生代会被完全回收,并且因为writebarrier性能消耗,新生代没有被记录)。老年代是指老年代的对象,Rset存放在老年代。oldgeneration指的是younggeneration对象,Rset保存在younggeneration中。PerRegionTable(PRT)RSet通过PRT记录分区的引用。当一个指针指向Rset(图中右上角)中的一个区间时,包含该指针的堆块就会被PRT标记。PRT需要内存空间来存储这些引用关系。根据引用次数,PRT有三种记录引用的模式,会根据调用次数变化:稀疏(hash)->细粒度->粗粒度。稀疏表:存储在哈希表中,key是区域索引,value是card数组。Fine-grainedPerRegionTable:当稀疏表指定的region中的卡片数量超过阈值时,在fine-grainedPRT中创建对应的PerRegionTable对象。一个Region地址链表,维护了当前Region所有卡片对应的BitMap集合。粗粒度位图:当细粒度PRT大小超过阈值时,所有区域组成一个位图。如果有一个region有指向当前Region的指针,则将其对应的位设置为1。CSet收集集合(CSet)表示每次GC暂停时回收的一系列目标分区。收集暂停期间,CSetRegion会被释放,存活的对象会分配到空闲分区。G1的集合是基于CSet的操作。新生代收集和混合收集没有明显区别。最大的区别在于两个集合的触发条件。新生代收集:当新生代空间增长到Eden满时,会触发一次STW式的新生代收集。Eden分区中存活的对象会被复制到Survivor分区;原Survivor分区中的存活对象会根据tenuringthreshold提升到PLAB、新Survivor分区和oldgeneration分区。原有的年轻代分区会被整体回收。混合集合:老年代的空间逐渐被填满。当老年代占用的空间超过整个堆比IHOP阈值-XX:InitiatingHeapOccupancyPercent(默认45%)时,G1将开始混合垃圾回收周期。为了减少暂停目标,混合收集将被分成批次并与用户线程交替执行。每次STW混合回收都类似于新生代回收过程。G1的活动周期工作流程:RSet维护G1通过维护RSet来记录对象分区之间的引用,避免全堆扫描。RSet通过两个方面来维护:WriteBarrier和ConcurrenceRefinementThreads。Writebarrierbarrier是指在native代码片段中,当执行某些语句时,也会执行barrier代码(类似于aop)。G1在赋值语句中主要使用Pre-WriteBarrier和Post-WriteBarrier。注意:写栅栏的指令序列开销是非常昂贵的,应用程序吞吐量会根据栅栏的复杂程度而降低。Barrierbeforewriting:在进行相等赋值之前,等式左边的原始引用会丢失一个引用,所以jvm需要在语句执行前记录丢失的引用对象。(不是立即执行,是分批次执行,使用SATB的方式)Post-writebarrier:赋值方程执行后,右边多了一个引用。需要确认是否需要添加引用,JVM不会马上处理,会先Record,后面再进行批处理(并发优化线程)。SATB+RSet解决什么问题?主要是解决并发标记过程中漏标、错标的问题。开头快照(SATB)——写前理解G1SATB和BarrierProcessing的增量更新算法SATB是并发标记阶段的增量全并发标记算法,适用于G1块堆结构。SATB算法将一开始标记的所有对象都视为存活对象,SATB会创建一个对象图,相当于堆的逻辑快照。所以可以按照图结构遍历对象(可以用RSet加速)。当一个被扫描的对象引用一个未被扫描的对象(黑色引用白色)时,通过writebarrier技术,将B到D的引用压入gc遍历执行的栈中,以便后面继续扫描D。开头的快照可以理解为,在扫描之前,将整个对象引用关系作为一个存活节点进行扫描。如果在扫描过程中B=null,说明D其实是垃圾,但是在后续的过程中D还是在处理过程中被标记为黑色。D本轮应该清零,但会暂时保留,在remark阶段处理。每个线程都有一个包含256条记录的SATB缓存。当它满了时,数据将被添加到全局列表中,并重新申请一批新的256个缓存。它还定期检查和处理全局缓冲区列表的记录,然后根据标志位图片段的标志位扫描参考字段以更新RSet。此过程也称为并发标记/SATB前写栅栏。ConcurrentRefinementThreads-Post-writebarrierprocessingwritebarrier(需要2个额外的指令)将更新CardTableType结构以跟踪代际引用。当writebarrier被触发时,会过滤是否是partition的对象。如果发生跨区域的应用程序更新,更新对象的卡片将被添加到缓冲区(新日志缓冲区或脏卡)。一旦日志缓冲区用完,就会分配一个新的日志缓冲区,并将原来的缓冲区添加到全局列表中。并发优化线程会一直处于活动状态,当全局列表中有数据时,会处理更新RSet。如果无法处理,则允许更多线程处理全局列表;如果无法处理,应用程序线程将被挂起处理全局列表。(Objectchangetoofrequently)并发标记周期(ConcurrentMarkingCycle)需要识别垃圾最多的老年代分区进行混合回收周期。整个循环完成根标记,识别可能存活的对象,计算分区活跃度,确定GC效率等级。触发条件:达到IHOP阈值(-XX:InitiatingHeapOccupancyPercent老年代整堆比例,默认45%),步骤(STW):初始标记(InitialMark)、根区域扫描(RootRegionScanning)、并发标记(ConcurrentMarking),重新标记(Remark)清理(Cleanup)并发标记线程(ConcurrentMarkingThreads)并发标记阶段,会对分区进行数据标记,会为一个分区创建两个位图来记录标记结果:PreviousBitmap,NextBitmap.PreviousBitmap对应上次标记(标记完成),NextBitmap对应本次标记结果(即将标记),使用两个变量记录(TAMS:Top-At-Mark_Start)标记的位置分别为:PreviousTAMS(PTAMS)上一次NextTAMS(NTAMS)下一次总的来说:区间内的数据通过两个Bitmap和两个‘指针’滚动循环,当循环完成后,再循环该区间。初始标记结束后:先将NextBitmap设置为空,将NTAMS设置为区间顶部。此时PTAMS还是上个大回合标记的位置。并发循环:PTAMS和NTAMS之间的数据会被标记,本段标记的结果+上次标记的结果会更新到NextBitmap。一个循环结束:将NextBitmap更新为PreviousBitmap,同时更新PTAMS和NTAMS的位置。在并发标记阶段,G1根据参数(-XX:ConcGCThreads(默认GC线程数的1/4,即-XX:ParallelGCThreads/4))分配线程,每个线程每次只扫描一个分区.并发标记周期(ConcurrentMarkingCycle)并发周期的过程是:初始标记、根分区扫描、并发标记、重新标记、清除阶段。初始标记(InitialMark):Exclusive,会触发STW,然后标记所有GCROOT可达的对象。当IHOP触发threshold时,G1并不会立即开始循环,而是等待下一次年轻代收集(这也需要STW),与STW之间的年轻代收集一起执行(piggybacking)。初始标记暂停时,将分区的NTAMS设置到分区顶部,并发执行初始标记,所有分区都会被处理。根区域扫描(RootRegionScanning)当STW结束时,年轻代收集和初始标记就完成了。基于标记算法,复制到幸存者区间的区间也需要被标记为根元素,G1会扫描这个区间,然后标记幸存者区间引用的对象。所以幸存区间也称为根区间。特殊:因为年轻代回收(被年轻代回收打断)在并发循环过程中会执行多次,所以需要在下一次中断前完成。并发标记(ConcurrentMarking)这个阶段与申请县同时执行。每个线程一次只扫描一个分区来标记幸存的对象图。在此阶段,处理Previous/Next标签位图,并扫描标签对象的引用字段。同时,并发标记线程还会处理SATB的全局列表信息。备注(Remark)最后一个标记动作将是独占(STW)阶段并且可以并行执行。在此阶段,将处理任何遗留SATB日志。查找所有未访问的活动对象。注意:引用处理也是remark阶段的一部分,所有大量使用引用对象(weak、soft、phantom、final)的应用程序都会在引用处理中产生开销。清理(??Cleanup)cleanup会识别并清理完全空闲的区域(RSet中的引用计数为空)并清理空闲区域。此阶段将处理Previous/Next标记位图和PTAMS/NTAMS。主要操作如下:RSet排序(根据扫描结果,确认RSet各个粒度的数据是否维护正确)整理堆分区,确定分区的访问热度,确定恢复收益高的分区以便后续跟进。识别空闲区间,直接回收。年轻代收集/混合收集周期年轻代收集(YoungCollection)年轻代由:Eden和Survivor组成,当Eden分配失败时触发,执行年轻代收集时中断(STW)。对象复制:根据CSet将Eden中存活的对象复制到Survivor空间。注意:所有年轻代对象(Eden和Survivor)都会被复制到新的Survivor空间。对象提升:如果一个对象经历的生存次数达到阈值,就会提升到老年代。该阈值是计算和配置的。分区调整:在收集的时候,G1会计算出当前年轻代需要扩容或者压??缩的总量。例如:freeintervalRSetsize当前最大可用的younggeneration当前最小可用的younggenerationPause时间信息维护:记录采集对象的年龄信息“AgeInfo”,方便后续提升到老年区,以及何时在混合收集阶段进行回收。UpdateRSet:会处理本地缓冲区中还没有提交到全局列表的RSet更新日志。扫描RSet:在收集当前CSet之前,需要扫描CSet分区的RSet,考虑到分区外的引用。ReleasePartition:ReleaseFreeCSet混合收集周期(MixedCollectionCycle)当一个并发标记周期完成后,一个混合收集周期就会开始。在这个循环中,G1不仅会收集新生代,还会同时收集老年代,而收集那些老年代,是根据老年代中垃圾的多少来决定的。单轮混合收集和新生代收集没有区别,但是因为老年代垃圾可能比较多,为了满足暂停要求,可能会连续进行多次混合收集。在此过程中,G1会计算下一次要处理的CSet集合的分区数,以及本次收集后是否需要结束循环。当FullGC无法申请到新的空间时,会进行STW风格的单线程FullGC。FullGC会标记清除并压缩整个堆,最后只会包含纯粹的幸存对象。FullGC的触发方式:从新生代分区复制存活对象时,找不到可用的空闲分区从老年代分区转移存活对象时,找不到可用空闲分区分配巨型对象时,无法findenoughcontinuouspartitionprocessesintheoldgeneration简述:最后,垃圾收集器可以说是Java的基石之一。垃圾回收器的实现充满了很多实现细节,对于一些优化有很大的参考价值。