java判断对象是否可回收依靠可达性分析算法:从gc roots出发追踪引用链,不可达对象先标记再筛选(如finalize未执行),两次失败才回收;新生代用复制算法因存活率低、效率高;老年代用标记-清除或标记-整理算法避免空间浪费;回收器选择需按场景——吞吐优先用parallel gc,响应敏感用zgc/shenandoah,大堆严苛延迟选zgc,并务必分析gc日志调优。

Java判断对象是否可回收靠什么算法
Java不用引用计数法,因为解决不了循环引用问题——objectA.instance = objectB 和 objectB.instance = objectA 同时置 null 后,两个对象仍互相持有引用,计数器不为 0,但已彻底不可达。JVM 实际用的是可达性分析算法:从一组固定的 GC Roots 出发(比如栈帧里的局部变量、静态字段、常量池项、JNI 引用),顺着引用链往下找;任何对象若跟所有 GC Roots 都断开连接,就判定为“不可达”,进入回收候选。
注意:不可达 ≠ 立刻回收。它要先被第一次标记,再经历一次筛选(比如是否重写了 finalize() 且未执行过),只有两次都“活”不下来,才真正进回收队列。
新生代为什么用复制算法
因为新生代里 95%+ 的对象“朝生夕死”,存活率极低。复制算法只搬运存活对象,效率高、无碎片、STW 时间短——这正是 Minor GC 追求的。
- HotSpot 虚拟机把新生代划为 Eden + 2 个 Survivor(
S0和S1),默认比例是8:1:1 - 每次 Minor GC 时,Eden 和其中一个 Survivor 中的存活对象被复制到另一个 Survivor;原区域直接清空
- 对象年龄达到阈值(默认 15)或 Survivor 放不下时,就会晋升到老年代
坑点:-XX:SurvivorRatio 调得太小(如设成 2),会导致 Survivor 空间不足,频繁提前晋升,加剧老年代压力;调太大又浪费空间,需结合实际对象生命周期压测调整。
立即学习“Java免费学习笔记(深入)”;
老年代为什么不用复制算法
老年代对象存活率高,如果还用复制算法,就得预留一整块同等大小的空闲内存来搬运,成本太高,也不现实。
所以主流策略是两种组合:
-
标记-清除:如 CMS(已废弃),快但碎片多,容易触发
Concurrent Mode Failure,退化为 Serial Old 全停顿回收 - 标记-整理:如 Serial Old、Parallel Old,会移动对象、压缩内存,避免碎片,但移动开销大,STW 更长
现代收集器如 G1、ZGC、Shenandoah 已不严格按“代”划分,但底层仍隐含类似逻辑:G1 把堆分成 Region,优先回收垃圾最多的 Region;ZGC 使用读屏障 + 颜色指针,在并发标记和转移中做到亚毫秒级停顿。
怎么选垃圾回收器和参数
没有万能配置,得看场景:
- 吞吐优先(后台批处理):用
-XX:+UseParallelGC,配-XX:MaxGCPauseMillis没意义,它只管吞吐量 - 响应敏感(Web API、实时系统):JDK 11+ 推荐
-XX:+UseZGC或-XX:+UseShenandoahGC;JDK 8 只能退而求其次用-XX:+UseG1GC并精细调优-XX:MaxGCPauseMillis和-XX:G1HeapRegionSize - 堆特别大(>64GB)且延迟要求严苛:ZGC 是目前最稳的选择,但需确认 OS 和 JDK 版本支持(如 Linux x64 + JDK 11+)
最容易被忽略的一点:GC 日志不是摆设。-Xlog:gc*:file=gc.log:time,tags,level(JDK 10+)能真实反映晋升频率、碎片程度、是否频繁 Full GC。没日志,调参就是蒙眼开车。








