一、垃圾回收为什么需要对象存活判定

想象你家的储物间堆满东西,有些经常用,有些早已积灰。JVM的内存就像这个储物间,需要定期清理"垃圾对象"。但怎么判断哪些对象该留?这就引出了两种经典算法:引用计数和可达性分析。

举个生活例子:

  • 引用计数:给每个物品贴便利贴,记录被借用的次数(比如书被3人借阅)
  • 可达性分析:检查物品能否从大门出发通过路径找到(比如玄关→客厅→茶几上的遥控器)

二、引用计数算法详解

工作原理

每个对象携带计数器,被引用时+1,引用失效时-1。当计数器归零,立即回收。

(示例代码使用Java技术栈)

public class ReferenceCounting {
    Object instance = null;
    
    public static void main(String[] args) {
        // 创建两个对象A和B
        ReferenceCounting A = new ReferenceCounting();
        ReferenceCounting B = new ReferenceCounting();
        
        // 互相引用(计数器变为1)
        A.instance = B;
        B.instance = A;
        
        // 断开外部引用(但A和B仍在互相引用)
        A = null;
        B = null;
        
        /* 此时:
           A和B的引用计数均为1 
           但实际已是垃圾对象 */
    }
}

优缺点分析

优点

  • 实时回收:计数器归零立刻清理,像超市货架即时补货
  • 低延迟:不需要全局扫描

缺点

  • 循环引用问题:像两个死党互相说"你先走",结果都走不掉
  • 频繁更新计数器:高并发时像超市收银台排长队

三、可达性分析算法详解

工作原理

从"GC Roots"(相当于地标建筑)出发,所有能走通的路径上的对象都存活,其余回收。GC Roots包括:

  • 栈帧中的局部变量
  • 静态变量
  • JNI引用

(示例代码使用Java技术栈)

public class ReachabilityAnalysis {
    static class Node {
        String name;
        Node next;
        
        Node(String name) { this.name = name; }
    }
    
    static Node globalHead = new Node("总部"); // GC Root
    
    public static void main(String[] args) {
        Node branchA = new Node("分公司A");
        Node branchB = new Node("分公司B");
        
        // 构建引用链
        globalHead.next = branchA;
        branchA.next = branchB;
        
        // 断开branchB的引用
        branchA.next = null;
        
        /* GC工作时:
           - globalHead → branchA 存活  
           - branchB 不可达被回收 */
    }
}

进阶应用:四种引用类型

  1. 强引用:像铁链连接(new创建的对象)
  2. 软引用:像橡皮筋连接,内存不足时断开(适合缓存)
  3. 弱引用:像棉线连接,GC即断开(实现WeakHashMap)
  4. 虚引用:像透明胶带,仅用于回收跟踪

四、两种算法的实战对比

场景对决

场景 引用计数表现 可达性分析表现
循环引用 内存泄漏 正确回收
高频创建/销毁 计数器成性能瓶颈 批量处理更高效
大内存应用 实时回收优势明显 停顿时间可能较长

现代JVM的选择

主流JVM(HotSpot等)都采用可达性分析,因为:

  1. 准确解决循环引用问题
  2. 配合分代收集更高效(年轻代用复制算法,老年代用标记-整理)

但某些场景仍会结合引用计数思想,比如:

  • Python的垃圾回收机制
  • Objective-C的ARC内存管理

五、开发中的注意事项

  1. 避免内存泄漏

    • 及时清除无用的监听器(如EventBus注册/反注册成对出现)
    • 小心静态集合(像永不清理的公告栏)
  2. 优化技巧

    // 不好的做法:频繁创建临时对象
    for(int i=0; i<10000; i++) {
        String temp = new String("item" + i); // 产生大量垃圾
    }
    
    // 改进方案:重用对象或使用基础类型
    StringBuilder sb = new StringBuilder();
    for(int i=0; i<10000; i++) {
        sb.setLength(0);
        sb.append("item").append(i); // 复用对象
    }
    
  3. 监控工具

    • jstat观察GC频率
    • VisualVM分析对象引用链

六、总结与选择建议

引用计数像精打细算的会计,可达性分析像全局视角的侦探。现代Java开发中:

  • 99%的情况交给JVM的可达性分析
  • 剩余1%需要关注特殊引用类型的使用

记住关键原则:让对象引用链尽可能短,像整理房间时减少物品的依赖关系。当你能清晰说出每个对象的"存活理由",就真正掌握了垃圾回收的精髓。