java弃用引用计数法因循环引用致内存泄漏、多线程更新开销大、内存浪费;gc roots包括栈中局部变量、静态变量、常量、jni引用及被锁对象;引用类型决定对象存活权重而非可达性。

Java为什么不用引用计数法做GC判定
因为循环引用会导致内存泄漏,JVM直接弃用——不是实现不了,而是它在Java这种强面向对象、大量使用复杂引用关系的场景下不可靠。
常见错误现象:a.next = b 且 b.next = a,然后 a = null; b = null;,这两个对象本该回收,但引用计数始终为1,永远卡住。
- 每次赋值、传参、出作用域都要原子更新计数器,多线程下开销大,还得加锁或CAS,拖慢吞吐
- 每个对象头上得多塞一个整型字段(通常4字节),堆内存浪费明显,对小对象尤其不划算
- Python能用是因为有
weakref兜底+周期性扫描辅助清理;Java选择从根出发一次性厘清关系,更稳
可达性分析里的GC Roots到底指哪些对象
GC Roots不是抽象概念,而是JVM明确定义的几类“绝对不能被回收”的活跃引用源,所有存活对象都必须能从它们出发被找到。
典型场景:你写了个static List<string> cache = new ArrayList()</string>,哪怕没其他代码访问它,这个cache也因是方法区静态变量而成为GC Roots,它引用的所有字符串都活得好好的。
立即学习“Java免费学习笔记(深入)”;
-
虚拟机栈中正在执行的方法的局部变量(比如Object obj = new Object()里的obj) -
方法区里的静态变量(static字段)、常量(final static String这类) -
本地方法栈中JNI调用持有的对象引用(如通过System.loadLibrary加载的native代码里还在用的对象) - 正在被同步块锁定的对象(
synchronized(obj)中的obj在锁未释放前也算Root)
可达性分析不是“实时扫描”,它有明确触发时机
很多人误以为对象一不可达就立刻回收——其实GC Roots遍历只发生在GC启动那一刻,中间发生的引用变化要等下次GC才生效。
容易踩的坑:在一次Full GC前疯狂创建短命对象并快速置null,但若它们恰好被某个长期存活对象临时引用过(比如进了缓存又删掉),就可能因“上次GC时还活着”而撑过本轮,撑到下一轮才被发现不可达。
- Young GC只扫描新生代,但GC Roots全量扫描(包括老年代里的静态变量),所以耗时比想象中高
- CMS和G1等并发收集器会用三色标记法分阶段处理,过程中需处理“对象在标记中被修改”的写屏障开销
- 不要依赖“对象变成null后马上释放内存”,JVM只保证“下次GC时可能回收”,不承诺时间点
引用类型(强/软/弱/虚)影响的是“是否算GC Roots链上的一环”
不是所有引用都一样硬。Java用四种引用语义控制对象在GC过程中的“存活权重”,核心是让GC策略更贴近业务意图。
例如用WeakHashMap做缓存,key是弱引用,只要没有强引用指向那个key,下一次GC就会把它连同对应value一起踢掉——这比手动维护清理逻辑干净得多。
-
强引用:最常见,如Object o = new Object(),只要它在GC Roots链上,就不会被回收 -
软引用:内存不足时才回收,适合缓存(SoftReference) -
弱引用:GC时不管内存足不足都回收,适合关联生命周期(WeakReference) -
虚引用:唯一用途是收到ReferenceQueue通知,用于堆外资源清理(PhantomReference)
真正复杂的是三色标记并发下的漏标问题,以及Finalizer机制带来的二次标记负担——这些细节藏在HotSpot源码里,日常开发只要记住:引用类型改变的不是“能不能被找到”,而是“找到后要不要留它一命”。










