每天分享一个小知识———JVM之引用计数与可达性算法分析

生活中的类比:彻底理解引用计数与可达性分析

为了更直观地理解这两个算法,我们通过生活中的常见场景进行类比。


1. 引用计数算法:图书馆借书模型

想象一个图书馆管理书籍的系统,每本书都有一个借阅计数器

  • 规则

    • 每当有人借书,计数器 +1

    • 还书时,计数器 -1

    • 当计数器归零时,书被下架(回收)。

  • 场景

    1. 学生 A 借了《算法导论》(计数器=1)。

    2. 学生 B 也借了这本书(计数器=2)。

    3. 学生 A 还书(计数器=1)。

    4. 学生 B 还书(计数器=0 → 书被下架)。

  • 循环引用问题

    • 假设《算法导论》的参考文献是《数据结构》,而《数据结构》的参考文献也是《算法导论》。

    • 两本书互相引用,但所有学生都已归还它们(计数器=1 → 无法归零)。

    • 结果:这两本书永远无法下架,占据书架空间(内存泄漏)。


2. 可达性分析算法:社交网络清理僵尸账号

假设某社交平台要清理不活跃的账号:

  • 规则

    • 核心用户(明星、官方账号等)出发,标记所有粉丝和好友。

    • 未被标记的账号视为“僵尸账号”,直接删除。

  • 场景

    1. 用户 A 和用户 B 互相关注,但他们没有关注任何核心用户。

    2. 平台从核心用户出发,遍历所有关联账号。

    3. 用户 A 和 B 无法从核心用户到达 → 标记为僵尸账号。

    4. 即使他们互相引用(关注),仍被删除。

  • 对比引用计数

    • 如果使用“关注计数器”:

      • 用户 A 和 B 的关注数均为 1(互相关注)。

      • 即使无人真正与他们互动,也不会被清理。

    • 可达性分析直接无视孤立的小圈子,只保留与核心关联的活跃用户。


十、终极总结:为什么 Java 选择可达性分析?

场景引用计数(图书馆模型)可达性分析(社交网络模型)
实时性立即下架归还的书定期批量清理僵尸账号
循环引用处理❌ 互相引用的书无法下架✅ 孤立圈子直接清除
性能代价管理员频繁更新计数器(琐碎操作)定期全面排查(短时集中资源)
适用场景小规模、实时性要求高的系统(如嵌入式)大规模、健壮性优先的系统(如 Java)

Java 的设计哲学
  • 牺牲实时性,换取健壮性:Java 面向企业级应用,内存泄漏的代价远高于短暂停顿。

  • 自动化内存管理:开发者无需手动处理对象生命周期,专注业务逻辑。

  • 高效处理复杂引用关系:现代应用对象关系复杂,可达性分析能可靠地清理垃圾。


十一、动手实验:验证垃圾回收行为

代码执行过程分析

public static void main(String[] args) {
    ReferenceCountingGC objA = new ReferenceCountingGC(); // 对象A被栈中局部变量objA引用(GC Root)
    ReferenceCountingGC objB = new ReferenceCountingGC(); // 对象B被栈中局部变量objB引用(GC Root)
    objA.instance = objB;  // 对象A的字段指向对象B
    objB.instance = objA;  // 对象B的字段指向对象A
    // 没有将objA和objB置为null
    System.gc(); // 触发垃圾回收
}

关键原因:对象仍被 GC Roots 引用

  1. GC Roots 的定义
    在垃圾回收时,JVM 会从一组称为 GC Roots 的起点出发,遍历所有可达对象。
    GC Roots 包括

    • 虚拟机栈中的局部变量(即当前方法中的 objA 和 objB)。

    • 方法区中的静态变量。

    • 本地方法栈中的 JNI 引用等。

  2. 对象可达性分析

    • 在代码执行到 System.gc() 时,objA 和 objB 未被置为 null,它们仍然是栈中的局部变量,属于 GC Roots

    • 即使 objA 和 objB 互相引用,它们仍然可以通过 GC Roots 到达,因此会被标记为存活对象。


内存结构示意图

栈(Stack)                            堆(Heap)
┌──────────────┐                     ┌──────────────────────┐
│ 局部变量objA  │ ──────────────────▶│ ReferenceCountingGC A │
│              │                     │  - instance → B      │
├──────────────┤                     └──────────▲───────────┘
│ 局部变量objB  │ ───────────────────────────────┘
└──────────────┘                     ┌──────────────────────┐
                                     │ ReferenceCountingGC B │
                                     │  - instance → A      │
                                     └──────────────────────┘
  • GC Roots(栈中的 objA 和 objB 始终指向堆中的对象 A 和 B。

  • 对象 A 和 B 互相引用,但它们仍然从 GC Roots 可达。


为什么不会被回收?

  1. 可达性分析的结果

    • 从 objA 和 objB(GC Roots)出发,可以遍历到对象 A 和 B。

    • JVM 会标记这些对象为存活状态,不会回收它们。

  2. 循环引用不影响可达性分析
    Java 的垃圾回收器通过可达性分析算法(而非引用计数),只要对象从 GC Roots 可达,即使存在循环引用,也不会被回收


对比实验:置为 null 后的回收

public static void main(String[] args) {
    ReferenceCountingGC objA = new ReferenceCountingGC();
    ReferenceCountingGC objB = new ReferenceCountingGC();
    objA.instance = objB;
    objB.instance = objA;
    objA = null; // 切断局部变量对对象A的引用
    objB = null; // 切断局部变量对对象B的引用
    System.gc(); // 对象A和B将被回收
}
  • 结果:内存被回收。

  • 原因
    objA 和 objB 被置为 null 后,堆中的对象 A 和 B 不再被 GC Roots 引用,虽然它们互相引用,但无法从 GC Roots 到达,因此被判定为垃圾。


验证方法:观察 GC 日志

  1. 添加 JVM 参数

    java -XX:+PrintGCDetails ReferenceCountingGC
  2. 不置为 null 时的日志

    [GC (System.gc()) [PSYoungGen: 1318K->488K(38400K)] 
    1318K->496K(125952K), 0.0011736 secs]
    • 内存未释放:堆内存从 1318KB 降至 496KB,但对象 A 和 B 占用的 2MB 内存未被回收。

  3. 置为 null 后的日志

    [GC (System.gc()) [PSYoungGen: 1318K->0K(38400K)] 
    1318K->210K(125952K), 0.0011736 secs]
    • 内存释放:堆内存从 1318KB 降至 210KB,对象 A 和 B 被回收。


总结

  • Java 的垃圾回收依赖可达性分析,而非引用计数

  • 只要对象被 GC Roots 引用(如局部变量),即使存在循环引用,也不会被回收

  • 切断 GC Roots 的引用后,循环引用的对象会被正确回收


通过生活中的类比和动手实验,相信你已经彻底理解引用计数与可达性分析的核心区别。关键在于:Java 通过“根搜索”无视孤立对象的内部引用,从根本上杜绝循环引用导致的内存泄漏。

💡 你的每个关注,都是我们共同进步的燃料!
⬇️ 下期预告:《每天分享一个小知识——JVM之强应用、弱引用、虚引用、软引用》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值