深入解析JVM垃圾回收:标记-压缩算法与LISP2实现机制
标记-压缩(Mark-Compact)算法是垃圾回收领域的重要基石。它在标记阶段与标记-清除(Mark-Sweep)算法完全一致,核心差异体现在随后的压缩阶段。该阶段的核心目标是将所有存活的对象向堆内存的一端进行滑动,使其紧凑排列。此举能够有效合并空闲内存块,彻底根治标记-清除算法带来的内存碎片化问题。
对象重排策略
在物理移动存活对象时,根据对象排列规则的不同,通常划分为三种策略:
- 无序移动:忽略对象原有的物理顺序及引用拓扑,随机将存活对象填入空闲区域。这种方式实现简单,但容易破坏内存访问的局部性原理。
- 拓扑排序移动:依据对象间的引用依赖图进行重排。若对象A引用对象B,则在物理内存上尽量保证A与B相邻,以优化缓存命中率。
- 滑动平移:严格保持对象在堆中的原有相对顺序,整体向内存一端滑动。目前主流的垃圾收集器多采用无序或滑动平移策略。
Two-Finger(二指)算法
该算法采用无序移动策略,是处理定长对象内存区域的经典方案。
执行逻辑:算法需对堆进行两次扫描。首次扫描时,将位于堆尾部的存活对象搬运至堆头部的空闲区域(这也是其依赖对象定长的原因)。第二次扫描负责修复因对象位移导致的引用失效。整个过程中,分配指针与回收指针分别从堆的两端向中心逼近,直至相遇。
LISP2 算法
相较于二指算法,LISP2 是一种适用性更广的滑动平移算法。它突破了对象必须定长的限制,能够处理变长对象。其核心特征是引入了额外的元数据空间来记录对象的"目标迁移地址",而非直接覆盖原对象内存。整个回收过程被严谨地拆分为三次独立的堆遍历。
第一次遍历:计算目标地址
回收器在此阶段仅负责计算并记录每个存活对象压缩后的目标物理地址。
// 阶段一:计算并记录存活对象的新地址
void phase1_computeAddresses(MemoryRegion heap) {
Pointer readCursor = heap.getStart();
Pointer writeCursor = heap.getStart();
while (readCursor < heap.getEnd()) {
if (readCursor.isMarked()) {
// 记录当前存活对象迁移后的目标地址
readCursor.setForwardingAddress(writeCursor);
// 写入指针向后偏移当前对象的大小
writeCursor = writeCursor + readCursor.getObjectSize();
}
// 读取指针向后偏移当前对象的大小
readCursor = readCursor + readCursor.getObjectSize();
}
}
逻辑解析:readCursor 和 writeCursor 初始均指向堆底。readCursor 负责遍历寻找存活对象。当发现存活对象时,将其目标地址记录在对象头(或额外空间)中,该目标地址即为当前 writeCursor 的位置。随后 writeCursor 增加该对象的尺寸,继续寻找下一个存活对象。遍历结束后,writeCursor 的位置即为所有存活对象压缩后的总边界。
第二次遍历:更新对象引用
此阶段旨在修复所有指向已移动对象的引用,使其指向新的目标地址。
// 阶段二:更新根节点及堆内对象的引用关系
void phase2_updateReferences(MemoryRegion heap, List<Pointer> roots) {
// 1. 修正根节点集合中的引用
for (Pointer rootField : roots) {
Pointer target = rootField.dereference();
if (target != null) {
rootField.update(target.getForwardingAddress());
}
}
// 2. 修正堆内对象之间的相互引用
Pointer cursor = heap.getStart();
while (cursor < heap.getEnd()) {
if (cursor.isMarked()) {
for (Pointer refField : cursor.getReferenceFields()) {
Pointer refTarget = refField.dereference();
if (refTarget != null) {
refField.update(refTarget.getForwardingAddress());
}
}
}
cursor = cursor + cursor.getObjectSize();
}
}
逻辑解析:首先遍历 GC Roots,将其中指向堆内对象的引用替换为对应对象的新地址。接着,通过 cursor 遍历整个堆,针对每个存活对象内部的引用字段,统一查表(即读取目标对象的 forwarding address)并更新为新地址。
第三次遍历:执行物理搬迁
最后一步是根据前两次遍历计算出的映射关系,执行实际的对象内存拷贝,并清除标记位。
// 阶段三:执行物理内存搬迁并清理标记
void phase3_relocateObjects(MemoryRegion heap) {
Pointer cursor = heap.getStart();
while (cursor < heap.getEnd()) {
if (cursor.isMarked()) {
Pointer destination = cursor.getForwardingAddress();
// 执行内存拷贝,将对象移动至目标紧凑区域
Memory.copy(destination, cursor, cursor.getObjectSize());
// 清除目标位置的标记位,为下一轮GC做准备
destination.clearMark();
}
cursor = cursor + cursor.getObjectSize();
}
}
逻辑解析:cursor 再次遍历堆内存,对于带有存活标记的对象,获取其记录的目标地址 destination,执行底层内存拷贝操作。搬迁完成后,立即清除新位置的标记状态,确保下一次垃圾回收周期的正确性。