一句话说透Java里面的JVM是怎么判断对象是否已死

182 阅读3分钟

一、核心原理:找风筝线

比喻:对象就像风筝,只要还有线(引用链)连着地面(GC Roots),就不是垃圾

1. 可达性分析算法(主流方案)

  • 步骤
    1️⃣ 扫描所有 GC Roots(地面上的固定桩)
    2️⃣ 沿着引用链标记所有可达对象(连着线的风筝)
    3️⃣ 未被标记的对象判定为可回收(断线的风筝)

  • 源码佐证:HotSpot 的 G1RootProcessor 类处理根节点枚举

    // hotspot/share/gc/g1/g1RootProcessor.cpp  
    void G1RootProcessor::process_strong_roots(...) {  
      // 遍历所有GC Roots类型  
      _process_strong_tasks->do_serial_threads();    // Java线程栈  
      _process_strong_tasks->do_universe();          // 系统类加载器  
      _process_strong_tasks->do_jni_handles();       // JNI全局引用  
    }  
    

2. GC Roots 具体包括

类型示例底层存储位置
虚拟机栈中的引用方法局部变量、参数各线程栈帧的局部变量表
本地方法栈中的引用JNI 本地方法引用的对象Native 方法栈
方法区静态引用static 修饰的类变量方法区的静态变量区
方法区常量引用final 修饰的常量方法区的运行时常量池
同步锁持有对象synchronized 锁对象对象头的Mark Word关联

二、对象标记技术:给风筝贴标签

1. 三色标记法(并发标记的关键)

  • 白色:未被扫描(可能垃圾)

  • 灰色:已扫描但子节点未扫描

  • 黑色:已扫描且子节点也完成扫描

  • HotSpot实现:使用位图(Bitmap)标记状态

    // hotspot/share/gc/shared/markBitMap.hpp  
    class MarkBitMap {  
      bool mark(HeapWord* addr) {  
        // 通过位运算设置标记位  
        *bit_index(addr) |= mask(addr);  
      }  
    };  
    

2. 漏标问题与解决方案

  • 问题场景:并发标记时,用户线程修改引用导致漏标

    原始状态:A(黑) → B(白)  
    用户操作:C(灰) → B(白),并删除 AB  
    结果:B被漏标  
    
  • 解决方案

    • 增量更新(CMS使用) :记录黑色对象新增的引用(把黑变灰)

    • 原始快照(G1使用) :记录灰色对象删除的引用(SATB队列)

    // G1的SATB处理(hotspot/share/gc/g1/g1SATBMarkQueue.hpp)  
    void G1SATBMarkQueue::enqueue(oop obj) {  
      if (!_buffer->is_full()) {  
        _buffer->push(obj);  // 记录被删除的引用  
      }  
    }  
    

三、四种引用类型:风筝线的材质

引用类型回收时机源码实现类使用场景
强引用永不回收(除非线断)默认普通对象
软引用内存不足时回收SoftReference缓存
弱引用下次GC必回收WeakReference临时缓存(如WeakHashMap)
虚引用随时回收,仅收尸通知PhantomReference堆外内存监控

示例代码

// 软引用示例(内存不足时才回收)  
SoftReference<byte[]> cache = new SoftReference<>(new byte[1024*1024]);  
if (cache.get() != null) {  
    // 缓存还有效  
}  

底层处理Reference 类的状态机(pending 列表)

// hotspot/share/classfile/javaClasses.cpp  
void java_lang_ref_Reference::notify_reference_reachability(...) {  
    // 将可达性变化的引用加入队列  
    enqueue_reference(ref);  
}  

四、finalize() 方法:临终抢救机会

特殊规则

  1. 对象第一次被标记为不可达时,会进入 F-Queue 队列
  2. Finalizer 线程异步执行 finalize() 方法
  3. 若在 finalize() 中重新建立引用,对象复活

源码流程

// hotspot/share/gc/shared/referenceProcessor.cpp  
void ReferenceProcessor::process_discovered_references(...) {  
  // 处理FinalReference  
  if (policy == ReferencePolicy::ALWAYS) {  
    enqueue_final_references(...);  
  }  
}  

致命问题

  • finalize() 执行时间不可控,可能阻塞 Finalizer 线程
  • 复活后再次死亡时不会再调用 finalize()

五、对象真正死亡的条件

必须同时满足:

  1. 不可达:从GC Roots出发无法访问到该对象
  2. 未复活finalize() 未成功重新建立引用
  3. 无关联:该对象的类已被卸载(非必须条件)

六、总结口诀

「判断生死看引用,可达分析找根源
三色标记防漏标,增量快照解疑难
软弱虚引用各不同,finalize可复活
对象头里藏标记,位图操作见真章!」

附:HotSpot 参数验证

  • -XX:+PrintGCDetails:查看GC日志中的对象回收情况
  • -XX:+TraceReferenceGC:追踪引用处理过程