JVM G1 源码分析01 分区 && 空间分配 && RememberSet && DirtyCard

JVM G1 源码分析01 分区 && 空间分配 && RememberSet && DirtyCard

HR的大小影响分配和回收的效率,HR过大则回收消费的时间长,HR过小则导致对象内存分配较慢且内存利用率较低。
HR大小上下限定义在heapRegionBounds中,最小1MB,最大32MB。HR的size必须是2的幂次,即仅可为1MB、2MB、4MB、8MB、16MB、32MB。

JVM参数-XX:G1HeapRegionSize可以指定HR size,如果未指定,JVM根据实际情况动态决定。

HR数量默认为2048。

计算HR size的逻辑主要在setup_heap_region_size函数中

计算初始堆内存和最大堆内存的平均值average_heap_size
取HR size下限(1MB)和average_heap_size / 2048的最大值,赋值给region_size
region_size按2的幂次对齐
根据region_size计算卡表的大小
 

新生代大小的计算逻辑主要在g1YoungGenSizer中

根据JVM参数-XX:G1NewSizePercent和-XX:G1MaxNewSizePercent计算新生代最小和最大的HR数量
4. 总结
HR是G1堆内存管理的核心模型,标记、回收、晋升、疏散、卡表、RSet等G1关键逻辑都围绕HR进行。
 

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

空间分配

当开发者使用JAVA语言实例化一个对象时,排除JIT的标量替换等优化手段,该对象会在JAVA Heap上分配存储空间。

分配空间时,为了提高JVM的运行效率,应当尽量减少临界区范围,避免全局锁。G1的通常的应用场景中,会存在大量的Mutator同时执行,为减少锁冲突,引入了TLAB(线程本地分配缓冲区 Thread Local Allocation Buffer)机制。

G1支持基于TLAB的快速分配,当TLAB快速分配失败时,使用TLAB外慢速分配。
空间分配的入口在instanceKlass.cpp

 

  • 判断是否重写了finalize方法,如是,则注册finalizer
  • 调用CollectedHeap::obj_allocate分配

JVM G1 源码分析01 分区 && 空间分配 && RememberSet && DirtyCard

3. TLAB分配
Eden区域是所有线程都可以访问的区域,为了快速分配内存,TLAB机制是通过给每个线程分配一个线程独享的缓冲区来减少锁的。

TLAB位于Eden区域中,所有的TLAB对于其他线程都是可见的,但是只有本地线程可以在其TLAB中分配空间。

另外,TLAB分配时,为了线程安全仍然需要加锁。

  • 在已有的TLAB中分配,逻辑较为简单,调用本地线程的tlab的allocate方法
  • 如果在已有的TLAB中分配失败,则调用allocate_inside_tlab_slow,执行TLAB慢分配

分配成功后,TLAB的top修改为当前的top+size

判断是否TLAB未分配空间大于阈值,该阈值可以通过JVM参数-XX:TLABRefillWasteFraction指定,默认64分之一。如果TLAB未分配空间大于阈值,则保留此TLAB,进入TLAB外慢分配;如小于,则抛弃此TLAB重新申请TLAB
动态计算TLAB的size,并在将要抛弃的TLAB中填充dummy对象
申请一个新的TLAB
在新的TLAB中分配对象并返回
 

可以通过JVM参数TLABSize设置TLAB使用的空间;如果未设置,则动态计算的,受JVM参数TLABWasteTargetPercent(默认1%)和当前线程数影响,约等于Eden空间 * 2 * 1%

填充dummy对象的目的是为了遍历HR时,不再需要一个字节一个字节的遍历,dummy对象是一个int[]。

  • 首先判断是否大对象,如否则分配
  • 如果size > region_size / 2,则判定为大对象

4. 慢分配

当TLAB分配失败时,进入慢速分配阶段。慢速分配首先需要尝试对Heap加锁,加锁成功后在TLAB外的YHR或HHR分配。

调用mem_allocate方法分配,如果成功则更新线程占用内存数

如果是大对象,则进入attempt_allocation_humongous;否则进入attempt_allocation

由于大对象分配可能导致堆占用快速增长,因此在分配之前先判断是否满足GC标记的条件
加heap lock锁
尝试分配,如果成功则返回
如果需要gc,则触发gc,gc成功后,回到加锁逻辑,重新开始分配直到成功,或者gc重试次数大于GCLockerRetryAllocationCount
使用CAS操作分配,如果可用空间大于对象容量则持续重试,否则退出重新选择region或触发GC。

综上所述,JVM G1的对象的堆空间分配首先在TLAB中进行;当TLAB分配失败后,则在HR中直接进行分配;仍然失败后,则触发GC。

+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

JVM G1 源码分析(三)- Remembered Sets

1. 简介
记录集Remembered Sets简称RSet,用于记录对象在不同分区之间的引用关系,目的是为了加速垃圾回收的速度,主要是加速标记阶段。

本文将详细介绍RSet的结构。

通常的,有两种记录引用关系的方式,PointOut和PointIn。
如果obj1.field1=obj2,如果是PointOut方式,则在obj1所在region的RSet记录obj2的位置;如果是PointIn方式,则在obj2所在region记录obj1的位置。G1采用的是PointIn方式。

一共有五种分区间的引用关系:

分区内引用
新生代分区Y1引用新生代分区Y2
新生代分区Y1引用老年代分区O1
老年代分区O1引用新生代分区Y1
老年代分区O1引用老年代分区O2
YGC时,GC root主要是两类:栈空间和老年代分区到新生代分区的引用关系。
Mixed GC时,由于仅回收部分老年代分区,老年代分区之间的引用关系也将被使用。

因此,我们仅需要记录两种引用关系:老年代分区引用新生代分区,老年代分区之间的引用。

2. RSet
由于PointIn模式的缺点,一个对象可能被引用的次数不固定,为了节约空间,G1采用了三级数据结构来存储:

稀疏表:通过哈希表来存储,key是region index,value是card数组
细粒度PerRegionTable:当稀疏表指定region的card数量超过阈值时,则在细粒度PRT中创建一个对应的PerRegionTable对象,其包含一个C heap位图,每一位对应一个card
粗粒度位图:当细粒度PRT size超过阈值时,则退化为分区位图,每一位表示对应分区有引用到当前分区
每个HeapRegion都包含了一个HeapRegionRemSet,每个HeapRegionRemSet都包含了一个OtherRegionsTable,引用数据就保存在这个OtherRegionsTable中。

我们通过添加引用来了解RSet的结构。

添加引用的入口在heapRegionRemSet.cpp中
 

大对象可能跨region,因此需要找到该对象的头部region
判断region index是否在粗粒度位图中,如是,则直接返回
在细粒度PRT中查找region index对应的记录,如有,则返回
在稀疏表中添加该region index和card,如果返回成功或found,则返回
如果_sparse_table.add_card返回overflow,则表示稀疏表对应region记录已超过阈值,则在细粒度PRT中添加region index和card
 

稀疏表主要逻辑在sparsePRT.hpp中

 

往稀疏表中添加引用的逻辑主要在两个add_card方法中:

根据region index从哈希表中找到SparsePRTEntry
在_cards中遍历,如果card已经记录,则返回found;
如果小于阈值,则添加card到_cards数组
如果大于等于阈值,则返回overflow
细粒度PRT主要逻辑在heapRegionRemSet.cpp的PerRegionTable类中

 

  • _bm是个C heap位图,每一位对应region中的一个card
  • 添加引用时,先根据card在_bm中找到对应bit位置,然后将该bit置为1
  • _occupied引用数量加1,如果是并行操作则使用原子指令加1

在串行和并行GC中,GC通过整堆扫描,来确定对象是否处于可达路径中。

然而G1为了避免整堆扫描,为每个分区记录了一个RSet,记录引用分区内对象的card索引。这样标记时,仅仅需要扫描对应分区的对应card中的对象是否可达即可,极大的提升了GC效率。

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

JVM G1 源码分析(四)- Dirty Card Queue Set

 

1. 简介
在上篇文章中,我们介绍了RSet的原理,当对象引用关系变化时,都需要更新RSet。为了不影响Mutator的性能,RSet的更新通常是异步进行的,这一异步更新操作需要引入DCQS(Dirty Card Queue Set)结构。

本文将分析DCQS的原理。

2. DCQS
2.1 写入DCQS
JVM声明了一个全局的静态结构G1BarrierSet,其中包含两个Queue Set,DirtyCardQueueSet和G1SATBMarkQueueSet,分别用于处理DCQS和STAB。
 

如果当前线程是Mutator线程,则调用当前线程DCQ的enqueue方法
否则,调用DCQS的_shared_dirty_card_queue的enqueue方法,_shared_dirty_card_queue是全局变量,调用enqueue前需要加锁
入队最后都会调用到DirtyCardQueue的enqueue方法,DirtyCardQueue是PtrQueue的子类,因此实际会调用PtrQueue的enqueue方法。


调用enqueue_known_active将对象入队

enqueue_known_active
判断是否buffer已满,如果已满则调用handle_zero_index
如果未满,则对象入队,_index减去size

handle_zero_index
判断是否全局DCQ,如是则将全局DCQ放入DCQS,并申请为全局DCQ申请新空间
否则,调用process_or_enqueue_complete_buffer处理DCQ

process_or_enqueue_complete_buffer
如果DCQS中队列数量超过阈值,则调用mut_process_buffer,由Mutator线程协助处理引用变更
否则调用enqueue_complete_buffer,有refine线程组处理引用变更

enqueue_complete_buffer
enqueue_complete_buffer的逻辑较简单,将DCQ加入链表,如果没有refine线程在工作,则通过全局monitor通知refine线程
 

与refine线程组处理逻辑类似,只不过是在Mutator线程中调用apply_closure_to_buffer函数执行

2.2 处理DCQS
不论是Refine线程和Mutator线程处理,最后都会调用到apply_closure_to_buffer函数执行具体逻辑,Mutator线程处理模式较为简单,直接同步调用即可。这里先介绍Refine线程处理模式。

JVM定义了ConcurrentGCThread作为GC线程的基类
 

  • 使用volatile变量_should_terminate,用于从run的死循环中退出
  • 使用模板方法模式,具体线程具体逻辑实现虚函数run_service

Refine线程的具体逻辑在g1ConcurrentRefineThread.cpp的run_service方法中

  • 等待Mutator线程或其他Refine线程唤醒
  • 通过do_refinement_step调用DCQS的apply_closure_to_completed_buffer和apply_closure_to_buffer

card size默认512B,card内的对象都需要扫描,因此有可能导致浮动垃圾。

    综上所述,当JAVA进程修改了对象引用关系后,如果被引用对象处于老年代中,则将数据写入DCQ中,当DCQ满后,将DCQ加入DCQS中,并由Refine线程组离线更新RSet。如果Refine线程组线程数量超过阈值时,由Mutator线程协助处理。

+++++++++++++++++++++++++++++++++++++++++++++++++

 

 

 

相关文章:

  • 2021-10-17
  • 2021-05-22
  • 2021-07-04
  • 2021-10-15
  • 2021-12-07
  • 2021-07-03
  • 2021-11-10
  • 2021-11-20
猜你喜欢
  • 2022-01-02
  • 2021-11-29
  • 2021-07-19
  • 2021-12-28
  • 2021-07-08
  • 2021-05-15
  • 2021-06-17
相关资源
相似解决方案