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分配
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线程协助处理。
+++++++++++++++++++++++++++++++++++++++++++++++++