记忆集(RSet)
RSet使用根对象引用的收集算法
-
ObjA.Filed = ObjB
- point out : 在ObjA中的RSet记录ObjB的位置
- point in : 在ObjB中的RSet记录ObjA的位置
- RSet使用的是point in
G1提供的3种回收算法:
- 新生代回收:总是收集所有新生代分区
- 混合回收:收集所有新生代分区和部分老生代分区
- Full GC:处理所有分区
分区之间的引用关系
这里指分区里有一个对象存在一个指针指向另一个分区的对象
- 分区内部有引用关系
- 新生代分区之间有引用关系
- 新生代分区到老生代分区之间有引用关系
- 老年代分区到新生代分区之间有引用关系
- 老年代分区之间有引用关系
RSet回收的缺点:
- 需要额外的内存空间;这部分通常是JVM最大的额外开销,一般在1%~20%。
- 可能导致浮动垃圾。(RSet里的内容可能已死亡,这个时候)
哪些需要记录在RSet中的引用关系
-
需要:
-
老年代分区到新生代分区之间的引用
-
YoungGC的时候有两种根
- 栈空间/全局空间变量的引用
- 老生代分区到新生代分区之间的引用
-
-
老年代分区到老年代分区之间的引用
混合GC的时候可能只有部分分区被回收,必须记录引用关系,快速找到哪些对象是活跃的。
-
-
不需要:
-
分区内部的引用关系。
对于一个分区来说,只有回收和不回收,回收的时候就会遍历整个分区,所以无需记录这种引用关系。
-
新生代分区之间的引用关系:
G1的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自动推断三个值大小。
- G1ConcRefinementGreenZone为ParallelGCThreads
- G1ConcRefinementYellowZone 为 G1ConcRefinementGreenZone 的3倍
- G1ConcRefinementRedZone为 G1ConcRefinementGreenZone 的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写屏障采用三重过滤不必要的写操作:
-
不记录新生代到新生代的引用或者新生代到老年代的引用。
- 因为在垃圾回收时,新生代的堆分区都会被回收
- 过滤同一个分区内部引用,在RSet处理时过滤。
- 过滤掉空引用,在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,表示不会周期性地收集信息。在生产中通常不会使用信息收集。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。