深入学习 G1回收器和JVM:Refine线程(4)

记忆集(RSet)

RSet使用 根对象引用的收集算法
  • ObjA.Filed = ObjB

    • point out : 在ObjA中的RSet记录ObjB的位置
    • point in : 在ObjB中的RSet记录ObjA的位置
    • RSet使用的是point in

G1提供的3种回收算法:

  1. 新生代回收:总是收集所有新生代分区
  2. 混合回收:收集所有新生代分区和部分老生代分区
  3. Full GC:处理所有分区

分区之间的引用关系

这里指分区里有一个对象存在一个指针指向另一个分区的对象
  1. 分区内部有引用关系
  2. 新生代分区之间有引用关系
  3. 新生代分区到老生代分区之间有引用关系
  4. 老年代分区到新生代分区之间有引用关系
  5. 老年代分区之间有引用关系

RSet回收的缺点:

  1. 需要额外的内存空间;这部分通常是JVM最大的额外开销,一般在1%~20%。
  2. 可能导致浮动垃圾。(RSet里的内容可能已死亡,这个时候)

哪些需要记录在RSet中的引用关系

  1. 需要:

    1. 老年代分区到新生代分区之间的引用

      • YoungGC的时候有两种根

        • 栈空间/全局空间变量的引用
        • 老生代分区到新生代分区之间的引用
    2. 老年代分区到老年代分区之间的引用

      混合GC的时候可能只有部分分区被回收,必须记录引用关系,快速找到哪些对象是活跃的。
  2. 不需要:

    1. 分区内部的引用关系。

      对于一个分区来说,只有回收和不回收,回收的时候就会遍历整个分区,所以无需记录这种引用关系。
    2. 新生代分区之间的引用关系:

      G1的3种回收算法都会处理所有的新生代分区,回收的时候会遍历所有的新生代分区。
    3. 新生代分区到老生代分区之间的引用

      对于YoungGC来说,针对的新生代,则无需关心;对于混合GC来说,会使用新生代分区作为根,那么遍历所有新生代分区自然能找到老年代;对于FullGC来说,所有分区都会被清理,无需关心引用关系。

RSet和卡表的关系

RSet记录引用者的地址
  • 我们如果直接记录对象地址,带来的问题就是RSet会急剧膨胀(一个对象被引用的次数不固定,可能很多也可能很少)。一个位可以表示512个字节区到被引用区的关系。RSet用分区的起始地址和位图表示一个分区所有的引用信息。
  • 在G1中,算法可以简化为找到需要收集的分区HR集合,所以YoungGC扫描Root Set和RSet就可以了。
  • 卡表是个全局表,作用并不是记录引用关系,而是记录该区域中对象垃圾回收过程中的状态信息,且能描述对象所处的内存区域块。

新数据结构 - PRT(Per region Table):分区记录所有引用者的信息

每个HR里都包含了一个PRT,它是通过HR中的一个结构 HeapRegionRemSet获得,而每个HeapRegionRemSet包含了一个OtherRegionsTable,也就PRT。
OtherRegionTable使用了3种粒度来描述引用
  • 稀疏PRT:通过哈希表来存储。默认长度是4。
  • 细粒度PRT:通过PRT指针的数组,数组长度可以指定,也可以自动根据计算得到。
  • 粗粒度:通过位图来指示,每一位表示对应的分区有引用到该分区数据结构。

Refine线程的功能和原理

Refine线程是G1新引入的并发线程池,线程默认数为 G1ConcTefinementThreads+1

Refine的两大功能:

  • 用于处理新生代分区的抽样,并在满足响应时间的指标下,更新YHR的数目。
  • 管理RSet。这个是Refine最主要的功能。

    • RSet的更新不是同步完成的,G1会把所有的引用关系放入一个队列中,称为Dirty Card Queue(DCQ),然后使用线程来消费这个队列以完成更新。
    • 实际上除了Refine线程更新RSet之外,GC线程或者Mutator也可能会更新RSet。
    • DCQ通过Dirty Card Queue Set(DCQS)来管理。
    • 为了能够并发处理,每个Refine线程只负责DCQS中的某几个DCQ

抽样线程

Refine线程池中的最后一个线程就是抽样线程,主要作用是用来设置新生代分区的个数,使G1满足垃圾回收的停顿预测时间。

管理RSet

G1使用Refine线程 异步维护和管理引用关系。
  • JVM声明了一个全局的静态变量 DirtyCardQueueSet(DCQS)
  • DCQS里面存放的是DCQ
  • 为了性能考虑,所有处理引用关系的线程共享一个DCQS。
  • 每个Mutator线程在初始化的时候都关联这个DCQS。
  • 每个Mutator线程都有一个私有的队列,队列的最大长度由G1UpdateBufferSize(默认256)决定,即默认最大存放256个引用关系对象。
  • 在Mutator线程中,如果产生新的对象的引用关系,则把引用者放入DCQ中,当满256个时,就会把这个DCQ放入DCQS中(放入的时候需要加锁)。
  • 如果没有满256个时,也可以手动提交(需要指明有多个引用关系)。
  • 当Refine线程忙不过来的时候,G1让Mutator帮忙处理引用变更。
  • Refine线程的个数可以有用户设置。

Mutator处理DCQ

  • DCQS的最大长度依赖与Refine线程的个数,最大为RedZone的个数。
  • DCQS里面的DCQ个数超过RedZone的个数时,Mutator就不能把这个DCQ放入set中,这时候Mutator就会直接处理这个队列的引用。

Refine线程的工作原理

  • Refine线程的初始化是在GC管理器初始化的时候进行。JVM通过wait和notify机制实现。

    • 从 0 到 n-1 线程(n表示线程个数),当前一个线程发现自己太忙,则启动后面一个。
    • 当线程发现自己太闲,则主动冻结自己。
    • 第0个线程什么时候被激活?

      • 当mutator线程尝试把DCQ放入DCQS时,如果发现0号线程没有被激活,则发送notify激活。
    • 所以第0个线程是由任意mutator线程激活,1 到 n-1 线程只能由前一个线程激活。所以0号线程等待的monitor是个全局变量,而 1 到 n-1线程中的monitor是局部变量。
    • RSet的更新流程简单总结就是:根据引用者找到被引用者,然后在被引用者的RSet中记录引用关系。
    • Refine线程执行的过程不会发生GC,所以不会产生对象的移动。
    • 有可能过多的RSet更新会导致mutator很慢(mutator会主动帮忙Refine线程处理)

Refinement Zone

我们可以设置多个Refine线程工作,在不同的负载下启用的线程不同。这个工作负载就通过Refinement Zone控制。
G1提供3个值, Green, Yellow, Red,将整个Queue Set分为4个区。姑且称为白,绿,黄,红
    • [0,Green),对于该区,Refine线程不处理,交给GC线程来处理DCQ。
  • 绿

    • [Green,Yellow),在该区中,Refine线程开始启动,根据Queue Set数值的大小启动不同的Refine线程来处理DCQ。
    • 使用参数G1ConcRefinementThresholdStep来控制每个Refine线程消费队列的步长,如果不设置,则自动推断为Refine线程+1
    • [Yellow,Red),在该区中,所有的Refine线程(除了抽样线程)都参与DCQ处理。
    • [Red, + ∞),在该区中,不仅所有的Refine线程参与处理RSet,而且连Mutator线程也参与处理。
这3个值通过三个参数处理,默认值都为0,如果不设置,则G1自动推断三个值大小。
  • G1ConcRefinementGreenZoneParallelGCThreads
  • G1ConcRefinementYellowZoneG1ConcRefinementGreenZone 的3倍
  • G1ConcRefinementRedZoneG1ConcRefinementGreenZone 的6倍

所有Refine线程是有几个线程?

  • 可以通过G1ConcRefinementThreads设置,默认为0
  • 没有设置的时候G1启发式推断,设置为ParallelGCThreads.
  • ParallelGCThreads也根据参数设置,默认为0。
  • ParallelGCThreads没有设置时,G1也通过启发式推断
  • ParallelGCThreads = ncpus(cpu内核个数)

    • 当ncpus <= 8,ncpus为 8 +(ncpus - 8)*5/8
    • 当ncpus > 8,ncpus为cpu内核个数
  • 假设 ParallelGCThreads = 4 ,G1ConcRefinementThreads =3

    • G1ConcRefinementThresholdStep = 黄区个数 - 绿区个数/(worknum + 1),自动推断为2
    • 则绿黄红个数为 4,12,24
    • 这里有4个Refine线程

      • 0号线程:DCQ超过4个的时候启动,低于4个终止
      • 1号线程:DCQ超过到达9个启动,低于6个终止
      • 2号线程:DCQ达到11个启动,低于8个终止
      • 3号线程:处理新生代的抽样
      • 当DCQ超过24个,Mutator开始帮忙处理DCQ

RSet涉及的写屏障

写屏障是指在改变特定内存的值时(实际上就是写入内存),额外执行的一些动作。
写屏障通常用于在运行时探测并记录回收相关指针,在回收器只回收堆中部分区域的时候,任何来自该区域外的指针都会被写屏障捕获,这些指针将会在垃圾回收的时候作为标记开始的根。
CMS中也是通过写屏障记录引用关系。
每一次将一个老年代对象的引用修改为指向新生代对象,都会被写屏障捕获并记录下来。因此在新生代回收的时候,就可以避免扫描整个老年代来查找根。

G1写屏障采用三重过滤不必要的写操作:

  1. 不记录新生代到新生代的引用或者新生代到老年代的引用。

    • 因为在垃圾回收时,新生代的堆分区都会被回收
  2. 过滤同一个分区内部引用,在RSet处理时过滤。
  3. 过滤掉空引用,在RSet处理时过滤。
过滤后就能使RSet的占用空间大大减少。
垃圾回收的写屏障使用一种两集的缓存结构(用queue set 实现)
  • 线程queue set : 每个线程有自己的queue set。所有线程都会把写屏障的记录先放入自己的queue set中,装满之后,就会把queue set 放入global set of filled queue中。而后再申请一个新的queue set。
  • global set of filled buffer:所有线程共享的一个全局的,存放填满了的DCQS集合。

参数介绍和调优

  • 参数G1ConcRefinementThreads,指的是G1Refine线程的个数,默认值为0,G1可以启发式推断,将并行的线程数ParallelGCThreads作为并发线程数,其中并行线程数可以设置,也可以启发式推断。通常大家不用设置这个参数,并行线程数可以简单总结为CPU个数的5/8,具体的推断方法见上文。
  • 参数G1UpdateBufferSize,指的是DCQ的长度,默认值是256,增大该值可以保存更多的待处理引用关系。
  • 参数G1UseAdaptiveConcRefinement,默认值为true,表示可以动态调整RefinementZone的数字区间,调整的依据在于RSet时间是否满足目标时间。
  • 参数G1RSetUpdatingPauseTimePercent,默认值为10,即RSet所用的全部时间不超过GC完成时间的10%。如果超过并且设置了参数G1UseAdaptiveConcRefinement为true,更新GreenZone的方法为:当RSet处理时间超过目标时间,Greenzone变成原来的0.9倍,否则如果更新的处理过的队列大于GreenZone,增大Greenzone为原来的1.1倍,否则不变;对于YellowZone和RedZone分别为GreenZone的3倍和6倍。这里特别要注意的是当动态变化时,可能导致GreenZone为0,那么YellowZone和RedZone都为0,如果这种情况发生,意味着Refine线程不再工作,利用Mutator来处理RSet,这通常绝非我们想要的结果。所以在设置的时候,可以关闭动态调整,或者设置合理的RSet处理时间。关闭动态调整需要有更好的经验,所以设置合理的RSet处理时间更为常见。
  • 参数G1ConcRefinementThresholdStep,默认值为0,如果没有定义G1会启发式推断,依赖于YellowZone和GreenZone。这个值表示的是多个更新RSet的Refine线程对于整个DirtyCardQueueSet的处理步长。
  • 参数G1ConcRefinementServiceIntervalMillis,默认值为300,表示RS对新生代的抽样线程间隔时间为300ms。
  • 参数G1ConcRefinementGreenZone,指定GreenZone的大小,默认值为0,G1可以启发式推断。如果设置为0,那么当动态调整关闭,将导致Refine工作线程不工作,如果不进行动态调整,意味着GC会处理所有的队列;如果该值不为0,表示Refine线程在每次工作时会留下这些区域,不处理这些RSet。这个值如果需要设置生效的话,要把动态调整关闭。通常并不设置这个参数。
  • 参数G1ConcRefinementYellowZone,指定YellowZone的大小,默认值为0,G1可以启发式推断,是GreenZone的3倍
  • 参数G1ConcRefinementRedZone,指定RedZone的大小,默认值为0,G1可以启发式推断,是GreenZone的6倍,通常来说并不需要调整G1ConcRefinementGreenZone、G1ConcRefinementYellowZone和G1ConcRefinementRedZone这3个参数,但是如果遇到RSet处理太慢的情况,也可以关闭G1UseAdaptiveConcRefinement,然后根据Refine线程数目设置合理的值。
  • 参数G1ConcRSLogCacheSize,默认值为10,即存储hotcard最多为210,也就是1024个。那么超过1024个该如何处理?实际上JVM设计得很简单,超过1024,直接把老的那个card拿出去处理,相当于认为它不再是hotcard。
  • 参数G1ConcRSHotCardLimit,默认值为4,当一个card被修改4次,则认为是hotcard,设计hotcard的目的是为了减少该对象修改的次数,因为RSet在被引用的分区存储,所以可能有多个对象引用这个对象,再处理这个对象的时候,可以一次性地把这多个对象都作为根。
  • 参数G1RSetRegionEntries,默认值为0,G1可以启发式推断。base*(log(region_size/1M)+1),base的默认值是256,base仅允许在开发版本设置,在发布版本不能更改base。这个值很关键,太小将会导致RSet的粒度从细变粗,导致追踪标记对象将花费更多的时间。另外,从上面的公式中也可以得到:通过调整HeapRegionSize来影响该值的推断,如人工设置HeapRegionSize。实际工作中也可以根据业务情况直接设置该值(如设置为1024);这样能保持较高的性能,此时每个分区中的细粒度卡表都使用1024项,所有分区中这一部分占用的额外空间加起来就是个不小的数字了,这也是为什么RSet浪费空间的地方。
  • 参数G1SummarizeRSetStats打印RSet的统计信息,G1SummarizeRSetStatsPeriod=n,表示GC每发生n次就统计一次,默认值是0,表示不会周期性地收集信息。在生产中通常不会使用信息收集。

你可能感兴趣的:(java,jvm,jvm调优,后端)