Java通过可达性分析判断对象是否可回收:从GC Roots(如虚拟机栈局部变量、方法区静态字段、本地方法栈JNI引用)出发,不可达的对象即被回收;新生代用复制算法因存活率低,老年代用标记-整理或清除因存活率高;Minor GC由Eden空间不足触发,Full GC由老年代/Metaspace不足等引发;GC日志中“GC (Allocation Failure)”表明因内存分配失败而触发GC。

Java GC怎么判断一个对象该回收了
Java不用引用计数,因为两个对象互相 instance 引用时,计数器永远不为 0,但实际已没人能访问——这种对象照样被回收,说明 JVM 根本不看计数器。
它用的是可达性分析:从一组叫 GC Roots 的对象出发,顺着引用链往下找。只要没链连到 GC Roots,就直接判“死”。这些 GC Roots 包括:
-
虚拟机栈里正在用的局部变量(比如方法参数、new出来还没出作用域的对象) -
方法区中的静态字段(static Object obj)和字符串常量(String s = "hello") -
本地方法栈里 JNI 调用中还在用的 Java 对象
注意:objA.instance = objB 这种赋值,如果 objA 本身已不可达,那 objB 也不会因此“活下来”——引用链必须能回溯到 GC Roots 才算数。
为什么新生代用复制算法,老年代却不用
因为新生代对象“朝生暮死”,98% 活不过一次 Minor GC;而老年代对象大多熬过多次 GC,存活率高。算法选型就是冲着这个分布来的。
立即学习“Java免费学习笔记(深入)”;
复制算法(如 Eden + Survivor)适合新生代:
- 只需复制存活对象,不用遍历全部内存,快
- 复制后天然紧凑,无碎片,下次分配大对象不卡壳
- 代价是浪费一半空间(
Survivor区只用一块),但新生代本来就不该存太多东西
老年代若也复制,就得搬动大量长期存活对象,停顿时间爆炸。所以主流收集器(SerialOld、G1 Mixed GC、ZGC)在老年代倾向用标记-整理,或带记忆集优化的标记-清除。
别硬记“新生代=复制”,要看本质:低存活率 → 复制划算;高存活率 → 整理或清除更稳。
Minor GC 和 Full GC 到底什么时候触发
GC 不定时,全看堆里“有没有地方放新对象”。不是“时间到了就收”,而是“放不下才收”。
Minor GC 触发最常见场景:
- 给新对象分配时,
Eden区没足够连续空间(哪怕总空闲很多,但碎片化也不行) - 每次 Minor GC 前,JVM 会检查老年代剩余空间是否 ≥ 新生代所有对象大小(空间分配担保)。不够?直接升级成
Full GC
Full GC 诱因更隐蔽,容易误判:
- 老年代空间不足(最常见)
-
Metaspace(元空间)扩容失败(尤其用了大量反射、动态类生成) - 显式调用
System.gc()(仅建议,JVM 可忽略;但 CMS 收集器默认响应,G1/ZGC 默认不响应) - CMS 并发失败(
concurrent mode failure)或晋升失败(Promotion Failed)
一个典型坑:频繁创建临时 byte[] 数组,虽短命,但可能因 Eden 太小、Survivor 容不下而提前晋升到老年代,很快撑爆老年代,引发 Full GC——这不是代码写错了,是堆参数没对齐业务特征。
GC 日志里看到 “GC (Allocation Failure)” 是什么意思
这是最真实的 GC 触发原因标签,出现在 OpenJDK 9+ 默认 GC 日志中,直白翻译就是:“我想分配对象,但内存不够,只好先 GC”。
它比“GC (System.gc())”或“GC (Metadata GC Threshold)”更有诊断价值,因为它暴露了真实瓶颈位置:
- 如果日志里全是
GC (Allocation Failure)+Eden区几乎清零 → 新生代太小或对象生命周期异常(比如缓存没设上限) - 如果同一轮 GC 后,
Old区使用率猛增 → 很可能是对象过早晋升(MaxTenuringThreshold设太低,或 Survivor 空间溢出) - 如果
Allocation Failure频繁伴随Metaspace使用率上涨 → 类加载泄漏(比如反复defineClass却没卸载)
别只盯着“GC 次数多”,先看 Allocation Failure 发生在哪一代、前后内存变化如何——这才是定位问题的起点。









