混合回收能够总结为两个阶段:
-
并发标记:算法
- 目的是识别老生代分区中的活跃对象,并计算分区中的垃圾对象占空间的多少,用于垃圾回收过程当中判断是否回收分区。
-
垃圾回收:并发
- 和新生代回收步骤一致,重用了新生代回收的代码,最大的不一样就是回收的时候不只仅回收新生代分区,同时回收并发标记中识别的垃圾多的老生代分区。
并发标记算法详解
并发标记算法是混合回收中最重要的算法。并发标记指的是标记线程和mutator线程并发运行。
并发标记算法设计了4个指针
- Bottom:底部位置
- Prev:指向上次并发处理后的地址
- Next:指向并发标记开始以前内存已经分配成功的地址
- Top:在并发标记开始后,若是有新的对象分配,能够移动top指针,使top指针指向当前内存分配成功的地址。
- Next和Top之间就是Mutator线程新增的对象使用的地址。
- 假设Prev以前的对象已经标记成功,在并发标记的时候从根出发,不只仅标记Prev和Next之间的对象,还标记Prev以前的活跃对象。当并发标记结束以后,只需将- Prev指针设置为Next指针便可开始新一轮的标记处理。
-
并发标记引入两个位图:性能
- PrevBitMap:记录Prev指针以前的内存标记情况
- NextBitMap:表示整个内存到next指针以前的标记状态
并发标记开始以前:
TAMS指的是Top-at-Mark-Start,并发标记结束后,NextBitMap标记了分区对象的存活状况。假定位图中黑色区域表示堆分区中对应的对象还活着,在并发标记的同时Mutator继续运行,因此Top会继续增加。优化
第二次标记开始,将NextBitMap值赋给PrevBitMap,将Next指针位置设置为Prev,将Top指针位置设置为Next指针。spa
并发标记结束状态
并发标记第二次开始前的状态
线程
并发标记第二次结束状态
设计
并发标记算法的难点
主要是GC在标记的时候,mutator线程可能正在改变对象的引用关系图,从而形成漏标和错标。3d
- 错标:不会影响程序正确性,只是会产生浮动垃圾
- 漏标:可能会致使可达对象被当成垃圾回收掉,从而影响程序的正确性。
三色标记法
三色标记法是一个逻辑上的抽象,将对象分红三种颜色
- 白色:表示尚未被收集器标记的对象
- 灰色:表示自身已经被标记到,但其拥有的field字段引用到的其余对象还没被处理
- 黑色:表示自身已经被标记到,且对象自己全部的field引用到的对象也已经被标记。
对象在并发标记阶段会被漏标的充分必要条件是:
- Mutator插入了一个从黑色对象到该白色对象的新引用,由于黑色对象已经被标记,若是不对黑色对象从新处理,那么白色对象将会被漏标,形成错误。
- Mutator删除了从灰色对象到该白色对象的直接或间接引用,由于灰色对象正在标记,字段引用的对象没有被标记,若是这个引用的白色对象被删除了(引用发生了变化,那么这个引用也有可能被漏标)。
要避免漏标,只要打破上面任意一个便可指针
- 经过增量更新算法关注对象的引用插入,把被更新的黑色活着白色的对象标记为灰色,打破第一个条件。
- SATB关注引用的删除,即在对象被复制前,把老的被引用对象记录下来,而后根据这些对象为根从新标记一遍,打破第二个条件。
混合回收的步骤
-
第一阶段:并发标记code
- 初始标记子阶段
- 并发标记子阶段
- 再标记子阶段
- 清理子阶段
-
第二阶段:垃圾回收
- 初始标记子阶段:负责标记全部直接可达的根对象(栈对象,全局对象,JNI对象等),根是对象图的起点,所以须要将Mutator线程暂停,须要一次STW。
- 并发标记子阶段:当YGC结束后,若是发现知足并发标记的条件,并发线程就开始并发标记。根据新生代的Survivor分区以及老生代的RSet开始并发标记。并发标记的时机是在YGC后,只有达到InitiatingHeapOccupancyPercent阈值后,才会触发并发标记。InitiatingHeapOccupancyPercent默认值是45,表示的是当已经分配的内存加上即将分配的内存超过内存总容量的45%就能够开始并发标记,并发标记会对全部分区进行标记,且并不须要STW。
-
再标记子阶段:是最后一个标记阶段,须要一个STW,找出全部未被访问的存活对象,同时完成存活内存数据计算。要结束标记,须要知足三个条件:
- 从根(Survivor)出发,并发标记子阶段已经追踪了全部的存活对象。
- 标记栈是空的。
- 全部的引用变动都被处理了;这里的引用变动包括新增空间的分配和引用变动,新增的空间全部对象都认为是活的,引用变动处理SATB。
最后一个阶段很难达成,若是不STW,应用会不断的更新引用,产生新的引用变动,则会永远没法结束标记。
-
清理子阶段:须要一个STW,清理子阶段主要执行如下操做:
- 统计存活对象:这是利用RSet和BitMap完成的,统计的结果将会用来排序分区gion,以用于下一次CSet的选择;根据SATB算法,须要把新分配的对象,都视为活跃对象。
- 交换标记位图:为下次并发准备
- 重制RSet:此时老年代分区已经标记完成,若是标记后的分区没有引用对象,这说明引用已经改变,这个时候能够删除原来的RSet里的引用关系。
- 把空闲分区放入分区列表中:这里的空间指的是全都是垃圾对象的分区,若是分区还有任何分区活跃对象都不会释放,真正的释放是在混合GC中。
清理操做不会清理垃圾对象,也不会执行存活对象的拷贝。
- 混合回收阶段的分析:选出若干个分区,将这些分区存活的对象复制到空闲的分区去,同时把这些已经被回收的分区放入空闲分区列表
- 并发标记的正确性分析:目的是为了识别老生代分区使用的状况,在下一次回收的时候优先选择垃圾比较多的分区进行回收。

GC活动图

并发标记是依赖于YGC,即并发标记发生前必定有一次YGC。在并发标记结束以后,会更新CSetChooser,此时若是在发生GC,则判断是否可以进行混合GC,混合GC的条件是上次发生的YGC不包含初始标记,而且CSetChooser包含有效的分区。
参数优化
- 参数InitiatingHeapOccupancyPercent(简称为IHOP,默认值为45,这个值是启动并发标记的先决条件,只有当老生代内存占总空间45%以后才会启动并发标记任务。增长该值,将致使并发标记可能花费更多的时间,也会致使YGC或者混合GC收集时收集的分区变少,但另外一方面就有可能致使FGC。根据经验这个值一般根据总体应用占用的平均内存来设置,能够把该值设置得比平均内存稍高一些,此时性能最好(即YGC/混合GC比较快,且FGC比较少)。那么如何获得应用程序在运行时的内存使用状况?能够打开G1PrintHeapRegions观察内存的分配和使用状况,另外JVM提供了一个诊断选项G1PrintRegionLivenessInfo,打开该选项,能够查看到内存的使用状况。IHOP的设置很是有用,可是设置合理的IHOP并不容易,须要不断地尝试。
- 参数G1ReservePercent,默认值为10,当发现GC晋升失败致使FGC,能够增大该值。
- 参数ConcGCThreads为并发线程数,默认值为0,若是没有设置则动态调整;使用ParallelGCThreads(前文介绍过推断依据)为依据来推断。ConcGCThreads=(ParallelGCThreads+2)/4,最小值为1,若是发现并发标记耗时较多能够增大该值,注意增大该值会致使Mutator执行的吞吐量变小。
- 参数HeapSizePerGCThread,默认值为64M,能够简单地理解为每64M分配一个线程。
- 参数UseDynamicNumberOfGCThreads,默认为false,打开该值表示能够动态调整线程数;调整的依据会根据最大线程数、HeapSizePerGCThread等肯定。
- 参数ForceDynamicNumberOfGCThreads,默认为false,打开该值表示能够动态调整,和UseDynamicNumberOfGCThreads功能相似。
- 参数G1SATBBufferSize,默认值为1K,表示每一个STAB队列最多存放1000个灰色对象,注意这里不是SATBqueueset的大小。
- 参数G1SATBBufferEnqueueingThresholdPercent(默认值是60),表示当一个队列满了以后,首先进行过滤处理,过滤后若是使用率超过这个阈值把队列送入到queueset并新分配一个队列。
- 参数MarkStackSize和MarkStackSizeMax,在32位JVM中设置为32k和4M,64位JVM中设置为4M和512M。若是没有设置能够启发式推断参数,确保MarkStackSize最小为32k(或者和并发线程参数ParallelGCThreads正相关,如ParallelGCThreads=8,则32位JVM中MarkStackSize=8×16k=128K,其中16k是队列的大小),这个参数是并发标记子阶段中用到的标记栈的大小。
- 参数GCDrainStackTargetSize,默认值为64,表示并发标记子阶段处理时为了保证处理的性能,一次标记的最多对象个数。
- 参数G1MixedGCLiveThresholdPercent,默认值85,用于判断分区可否被加入到CSet中,低于该值将会被加入。
- 参数G1HeapWastePercent,默认值5,即当CSet中可回收空间的占总空间的比例大于G1HeapWastePercent才会开始混合收集。
- 参数G1MixedGCCountTarget,默认值为8,这个参数越大,收集老生代的分区越少,反之收集的分区越多。要保持老生代分区在CSet中的比例超过1/G1MixedGCCountTarget。
- 参数G1OldCSetRegionThresholdPercent,参数默认值是10,即一次最多收集10%的分区。
- 参数G1ConcMarkStepDurationMillis,默认值为10,表示每一个并发标记子阶段每次最多执行10ms。
- 参数G1UseConcMarkReferenceProcessing,默认值为true,打开表示在并发标记的时候能够标记引用。
- 在打开引用处理时,每次标记处理引用的对象数由G1RefProcDrainInterval控制,默认值为10。
- 参数ClassUnloadingWithConcurrentMark,默认值为true,打开表示在并发标记的时候能够卸载已经加载的类。