Shenandoah收集器的并发压缩整理能力是其区别于传统垃圾收集器的核心优势,这个功能完全依赖转发指针机制实现。转发指针本质是每个对象头中新增的一个引用字段,用于指向对象当前实际的内存地址,当对象被移动后,原有地址的转发指针会指向新的存储位置,保证所有访问都能找到对象的最新副本。

转发指针的基本设计
在Shenandoah的实现中,每个Java对象都会额外占用一个字长的空间存储转发指针,这个指针的初始值指向对象自身。当GC阶段需要移动对象时,会先在新的内存区域分配对象副本,然后将原对象的转发指针修改为指向新副本的地址,这个过程是并发安全的,不会阻塞业务线程的正常执行。
转发指针的访问逻辑会被植入到所有对象访问的字节码指令中,比如当我们执行Object_field指令读取对象字段时,JVM会先检查对象的转发指针是否指向自身,如果不是,就先跳转到转发指针指向的新地址再执行后续操作,这个检查逻辑的开销非常低,不会对应用性能造成明显影响。
并发压缩整理的完整流程
1. 初始标记与转发指针初始化
GC启动后首先进行初始标记,标记出所有存活对象,此时所有存活对象的转发指针都指向自身,还没有发生对象移动。初始标记阶段会停顿业务线程,但这个停顿时间只和GC Roots的数量相关,和堆大小无关。
2. 并发复制阶段
初始标记完成后进入并发复制阶段,GC线程会遍历所有标记的存活对象,为每个对象在新的内存区域分配空间,复制对象的所有字段到新空间,然后将原对象的转发指针更新为新对象的地址。这个阶段是和业务线程并发执行的,业务线程可能同时访问正在被复制的对象。
为了保证并发复制的正确性,Shenandoah使用了CAS操作来更新转发指针,只有第一个成功更新转发指针的线程(不管是GC线程还是业务线程)才能决定对象的最终存储位置。如果业务线程先访问了原对象,发现转发指针还是指向自身,就会尝试复制对象并更新转发指针,之后其他线程访问时就会直接跳转到新对象地址。
3. 并发引用更新阶段
所有对象复制完成后,需要更新堆中所有指向原对象的引用,让它们指向新的对象地址。这个更新也是并发执行的,GC线程会遍历堆中的所有引用,检查引用指向的对象的转发指针,如果转发指针指向其他地址,就把引用修改为转发指针指向的新地址。
4. 最终标记与清理阶段
引用更新完成后进行最终标记,处理并发阶段产生的新引用,然后清理掉原有的旧对象内存区域,完成整个并发压缩整理的过程。
转发指针的代码示例
我们可以通过JVM的测试代码来理解转发指针的工作逻辑,以下是一个简化的转发指针访问伪代码:
// 简化的对象访问逻辑,模拟转发指针的检查过程
public class ShenandoahAccessExample {
// 模拟对象头中的转发指针字段
static class ObjectHeader {
// 转发指针,初始指向自身
Object forwardPointer;
// 对象其他字段
int value;
}
// 访问对象字段的方法,模拟JVM的字节码执行逻辑
public static int readObjectValue(ObjectHeader obj) {
// 第一步:检查转发指针,如果指向其他对象,就跳转到新对象
ObjectHeader currentObj = obj;
while (currentObj.forwardPointer != currentObj) {
currentObj = (ObjectHeader) currentObj.forwardPointer;
}
// 第二步:读取对象的实际字段
return currentObj.value;
}
// 模拟GC线程复制对象的过程
public static void copyObject(ObjectHeader oldObj, ObjectHeader newObj) {
// 复制对象字段到新对象
newObj.value = oldObj.value;
newObj.forwardPointer = newObj;
// 使用CAS更新原对象的转发指针,保证并发安全
// 这里简化为直接赋值,实际实现使用Unsafe的CAS操作
oldObj.forwardPointer = newObj;
}
public static void main(String[] args) {
// 初始化对象,转发指针指向自身
ObjectHeader obj = new ObjectHeader();
obj.forwardPointer = obj;
obj.value = 100;
// 模拟GC线程复制对象
ObjectHeader newObj = new ObjectHeader();
copyObject(obj, newObj);
// 业务线程访问对象,会自动跳转到新对象
int result = readObjectValue(obj);
System.out.println("读取到的对象值:" + result);
}
}
转发指针的注意事项
- 转发指针会增加每个对象的内存开销,对于小对象来说这个开销的比例会比较明显,所以Shenandoah更适合大堆场景。
- 转发指针的检查逻辑会嵌入到所有对象访问指令中,虽然单个检查开销很低,但高频的对象访问还是会累积一定的性能损耗。
- 并发复制阶段可能出现业务线程和GC线程同时复制同一个对象的情况,CAS操作保证了只有第一个复制的线程生效,不会出现数据不一致的问题。
转发指针的设计是Shenandoah实现并发压缩的核心,它把对象移动和引用更新的过程从停顿业务线程改成了并发执行,大幅降低了GC的停顿时间,特别适合对延迟敏感的应用场景。
Shenandoah收集器转发指针并发压缩整理GC算法修改时间:2026-06-12 12:51:34