垃圾回收器
2025-09-24 16:16:06 0 举报
JVM 内部的垃圾回收器以及 G1 的回收原理
作者其他创作
大纲/内容
ParallelGC 线程1应用程序线程暂停
E
region_1新生代
Survivor Space
O
512B
ParallelGC 线程2应用程序线程暂停
region_0
1
0
Thread_3
TLAB 剩余空间是否足够
分配成功
S
结束
Y
-XX:MaxGCPauseMillis控制最大的垃圾收集停顿时间,-XX:GCRatio直接设置吞吐量的大小。
5
FGC
Thread_1
慢速分配
对于 G1 的 YGC 针对与新生代,无需引用;对于混合 GC 新生代分区 G1 会使用新生代作为根节点,遍历的时候自然可以找到 老年代;对于 FGC 更无需这个引用关系,所有分区都会被处理;
重置线程
N
CardTable
...
TLAB 快速分配
Thread_2
0X6290
Eden Space
Old Generation
重新标记
对象慢速分配
Obj 4
新生代到新生代
TLAB 分配线程_1
大对象分配
Value
region_1
RSet 记忆集
Serial GC 线程应用程序线程暂停
G1将堆划分为多个固定大小的Region(例如2M)。在G1中,如果一个对象的大小超过了单个Region容量的一半,它就被定义为一个“Humongous对象”(巨型对象)。传统GC: 年轻代和老年代的大小是固定的(虽然可以动态调整,但有上限)。可能会出现年轻代GC很频繁,而老年代却很空闲;或者一次晋升潮导致老年代不够用而年轻代却很空闲的情况。内存使用不够高效。G1:由于Region的角色不固定,G1可以根据每次GC的实际情况,动态地决定分配多少Region给Eden,多少给Survivor,多少给Old。这使得内存使用率更高,更加“按需分配”。
堆
H
对象是否是大对象
region_2
Humongous
region_3老年代
0X9527
加锁
老年代 region_1 地址
分配新的 TLAB
YangGC 回收(收集所有新生代分区)
老年代 region_2 地址
超过垃圾回收的最大次数
JVM使用 CAS 锁进行分配固定大小的内存区域作为私有缓冲区
Obj 1
新生代 Region_2
Card Page_0
记忆集&卡表
记录对象在不同代际之间的引用关系,目的是为了加速垃圾回收的速度。在其之前 JVM 使用根对象引用的收集算法,既从跟集合出发,标记所有存活的对象,但是对于分代GC 当中这种做法很明显是低效的,因为我们如果只收集新生代,这种根可达标记同样的会扫描一遍老年代,而对于只收集老年代也会出现同样的类似的问题。所以对于设计者而言,就需要一个 RSet 记录从非收集部分指向收集部分的指针集合,使用这个集合描述对象的引用关系,从而保证欸快速、准确的找到来自堆的其他部分的、指向 Region 内的对象的引用,而无需扫描整个堆,
Serial Old收集器
region_2老年代
新生代 Region_0
既然 bit 位表示占用内存空间更小为什么不使用 bit 而使用 一个字节 表示呢?1.写入性能与写屏障开销,使用一个字节操作极其简单,脏化一个卡表只需要一条简单内存写入指令 card_table[card_index] = 0;由于对于一个字节的写入天然就是原子性,所以无需加锁,速度极快2.扫描性能,使用字节表示只需要遍历数组进行脏卡的查找,但如果对于 bit 位来说这个就变得相当麻烦,需要通过掩码技巧来加速查找遍历3.实现的复杂度,字节数组的数据结构相比于 bit 位这种结构更加直观简单,也便于代码的调试维护,但对于 bit 位来说所有的操作都是需要位运行来完成,并且还存在并发的问题
Key: 当前引用了其他代区域地址Value: 一个数组,数组内容是该代区域内的引用对象地址
Stop The World
ParNew GC 线程3应用程序线程暂停
对象分配
region_1老年代
并发低停顿,一种以获取最短回收停顿时间为目标的收集器。缺点:1.CMS收集器对处理器资源非常敏感,CMS默认启动的回收线程数是(处理器核心数量+3)/4,核心数量不足四个时,CMS对用户程序的影响就可能变得很大。如果应用本来的处理器负载就很高,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然大幅降低。2.font color=\"#ec7270\
TLAB 分配线程_3
分配对象
CMS收集器
0x00干净卡 - 没有包含任何被修改的引用字段
G1收集算法
Eempty Space
在 YGC 的时候,当一个老年代Region A中的对象,持有一个指向Eden Region B中对象的引用。如果不记录为了找到这个引用,确保Eden Region B中的那个对象不被错误回收,GC就不得不扫描整个老年代(可能有几个G到几十个G大)的所有对象。这会让一次本该很快的Young GC变得极慢。
G1 堆内存
老年代标记-整理算法
粗粒度位图
Parallel收集器
ParNew GC 线程1应用程序线程暂停
在进行Mixed GC时,如果存在一个在老年代Region X(不在本次CSet中)中的对象,持有一个指向老年代Region Y(在本次CSet中,即将被回收)中对象的引用,如果不记录为了找到这个引用,确保Region Y中的对象不被错误回收,GC就不得不扫描所有不在CSet中的老年代Region。
ParNew GC 线程2应用程序线程暂停
//使用字节数组遍历for (size_t i = 0; i card_table_size_in_bytes; i++) { if (card_table[i] == DIRTY) { // 一次简单的比较 scan_card(i); }}
默认 MaxGCPauseMillis = 200ms
ParallelGC 线程3应用程序线程暂停
TLAB 分配线程_2
region_1自由分区
Obj5
Obj 3
老年代到老年代
region_3新生代
分布内部
G1 收集器
新生代复制算法
快速分配和慢速分配
返回 NULL ,失败
region_2新生代
TLAB 撞针分配
Obj 6
......
参考:https://www.cnblogs.com/wwjj4811/p/17140968.html
老年代到新生代
Java 堆区
Key
bitMap
Obj3
0X8848
MixedGC 回收(会收集所有的新生代分区以及部分老生代分区)
... ...
老年代 Region_0
Full GC (对所有的分区处理)
老年代
Obj 0
NULL
Obj 2
新生代
ParNew收集器
TLAB 属于线程私有,分配一个对象优先从当前线程的 TLAB 中分配,由于不需要锁,可以达到快速分配的目的
TLAB 分配线程_4
region_0 新生代
Serial收集器
分代之间引用关系
Obj4
CardTable 指定的 region 当中的 card 数量超过一定的阈值时,会从粗粒度转为细粒度位图
region_3自由分区
这3种算法都会全量变量处理新生代,无需记录引用关系
CMS 线程1应用程序线程暂停
特殊状态卡:在垃圾回收过程的不同阶段,JVM 会使用这些值来跟踪 Card 的处理状态,以避免重复工作或进行优化。例如,并发标记
新生成代到老年代
Obj 5
//bit位 card_table 是一个 uint64_t 数组for (size_t i = 0; i card_table_size_in_qwords; i++) { uint64_t chunk = card_table[i]; size_t base_index = i * 64; // 当前 chunk 代表的起始 Card 索引 while (chunk != 0) { // 1. 找到最低位 1 的位置 int bit_pos = __builtin_ctzll(chunk); // 2. 计算实际的 Card 索引 size_t card_index = base_index + bit_pos; // 3. 处理这个脏 Card (这是耗时的操作) scan_card(card_index); // 4. 清除已处理的最低位 1 chunk = chunk & (chunk - 1); }}
并发标记
初始标记
记忆集
并发清除
region_2自由分区
Serial 收集器特性一样,Parallel 更注重可控制的吞吐量
Obj 7
Parallel Old收集器
因为回收都是针对一个分区而言所以,无需记录引用关系
细粒度位图
heap堆
0x01脏卡 - 这是由写屏障设置
... ...
0 条评论
下一页