主流虚拟机采用可达性分析而非引用计数,根本原因是引用计数无法解决循环引用导致的内存泄漏——A与B互相引用时计数器永不归零,造成不可逆OOM;而可达性分析从GC Roots出发遍历,天然规避循环陷阱,且配合写屏障等机制可确保标记精度。
主流虚拟机放弃引用计数、采用可达性分析,根本原因不是技术“先进性”的选择,而是为解决循环引用导致的内存泄漏这一硬伤——它在java这类面向对象语言中无法回避,且一旦发生就不可逆。
循环引用直接导致内存泄漏
引用计数法要求每个对象维护一个整型计数器,每次赋值或置空都需原子更新。但当对象A持有B、B又持有A时,即使外部所有引用(如栈变量)均已清空,A和B的计数器仍各为1,永远不为0。虚拟机无法判定它们已脱离业务逻辑,只能长期驻留堆中。这种泄漏在长周期服务(如Web容器、中间件)中会持续累积,最终触发OOM。
- 典型场景:监听器注册后未解绑、内部类隐式持外层引用、缓存Map中key-value互引
- 实测验证:《深入理解Java虚拟机》中经典示例运行后,System.gc()仍能回收互相引用的objA/objB,反向证明HotSpot未使用引用计数
并发与性能代价被低估
引用计数看似“即刻回收”,但在多线程环境下,每次引用赋值(obj.field = other)或局部变量重赋(x = y)都需对目标对象计数器做CAS操作。高并发业务中,这会引发大量缓存行失效(false sharing)和锁竞争。HotSpot实测表明,该开销在吞吐密集型应用中可抬升GC线程外的CPU消耗达12%以上。
- 对比:可达性分析仅在GC停顿窗口内集中遍历,引用关系变更无需实时同步
- 补充:Python虽用引用计数,但仅对不可含引用的简单类型(str/int)启用;list/dict等复合结构必须配合mark-sweep兜底,本质是混合策略
GC Roots提供稳定、可枚举的起点
可达性分析依赖一组明确、静态可识别的根集合(GC Roots),包括栈帧局部变量、静态字段、常量池项、JNI句柄等。这些位置天然具备“强存活”属性,且生命周期与线程/类加载器绑定,无需额外维护状态。从它们出发做图遍历,既能覆盖全部活跃引用路径,又能天然规避循环陷阱——A→B→A这条环形链,只要A、B均未被任何Root触达,整条链就被整体判为不可达。
- 关键细节:GC Roots本身位于堆外(栈、方法区、本地方法栈),确保分析过程不依赖堆内对象状态
- 延伸影响:该设计为分代收集、增量更新(如CMS、G1的SATB)提供了基础支撑
误标与漏标问题已有成熟应对
有人质疑可达性分析存在“漏标”(存活对象被错杀)或“误标”(死亡对象被错活),但这属于并发标记阶段的工程问题,而非算法缺陷。现代虚拟机通过写屏障(Write Barrier)、三色标记法、增量更新(IU)或原始快照(SATB)等机制严格约束,使标记精度趋近于100%。而引用计数的循环引用问题,是算法层面无解的结构性缺陷。
- 例如G1在并发标记时,用SATB记录被修改的引用字段,确保不会遗漏新建立的引用链
- ZGC更进一步,将标记过程完全并发化,停顿时间压至10ms内,仍未动摇可达性分析根基

















