深刻学习 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,表示不会周期性地收集信息。在生产中一般不会使用信息收集。
相关文章
相关标签/搜索