GC Roots是可达性分析的绝对存活起点,包括虚拟机栈局部变量、方法区静态/常量引用、本地方法栈JNI对象及被synchronized锁住的对象。

OpenJDK 的垃圾回收不是靠单一机制运转的,而是由「可达性分析 + 分代管理 + 多种收集器协同」共同完成的。它不保证对象一不可达就立刻回收,也不按你写的 new 顺序清理——回收时机、范围和方式,全取决于当前堆状态、收集器类型和 JVM 参数配置。
GC Roots 是什么,哪些东西能当 Roots
可达性分析的起点必须是“绝对存活”的对象,这些就是 GC Roots。它们本身不会被回收,且必须能被 JVM 瞬间定位。
-
虚拟机栈中的局部变量(比如方法参数、Object obj这样的引用) -
方法区中的静态字段(static Object holder)和常量(字符串常量池里的"hello") -
本地方法栈中 JNI 引用的对象(比如通过System.loadLibrary加载的 native 资源句柄) - 正在被
synchronized锁住的对象(JVM 内部会临时将其加入 Roots)
注意:finalizer 队列里的对象、软/弱引用本身不是 Roots;它们只是影响回收时机的“条件开关”,不是判定起点。
新生代回收(Minor GC)为什么快,但容易被误判为“没效果”
Minor GC 只处理 Eden 和一块 Survivor 区,采用复制算法:把存活对象拷到另一块 Survivor 或直接晋升到老年代,然后整块 Eden 一清而空。这决定了它快,但也带来几个易踩的坑:
- 大对象(>½ region size,G1 下;或 >
-XX:PretenureSizeThreshold,Parallel 下)会跳过 Eden,直接进老年代——你看到 Minor GC 频次低,未必是对象少,可能是它们根本没走这条路 -
Survivor空间不足时,部分存活对象会“担保失败”提前晋升,加剧老年代压力,后续可能触发Full GC - TLAB(Thread-Local Allocation Buffer)让线程在栈上预分配小块堆内存,避免锁竞争;但 TLAB 溢出后仍要走共享 Eden,监控时别只看 Eden 使用率,还要看
TLAB waste日志
G1 / ZGC / Shenandoah 的核心差异不在“要不要停顿”,而在“谁来承担停顿成本”
从 JDK 21 LTS 开始,G1 是默认收集器,ZGC 和 Shenandoah 是可选的低延迟方案。它们的关键区别不是“有没有 STW”,而是 STW 做了什么:
-
G1:大部分标记工作并发进行,但Initial Mark和Remark阶段仍需 STW;停顿时间可控(默认目标 200ms),但吞吐量有折损 -
ZGC:仅在Initial Mark和Final Mark有极短 STW(colored pointers)和更强的 CPU 占用 -
Shenandoah:用Brooks pointer实现并发移动,STW 更少,但对堆外内存(如 DirectByteBuffer)的跟踪更重,-XX:+UseShenandoahGC必须配合-XX:ShenandoahUncommitDelay等调参才稳
别指望换收集器就自动解决 OOM——如果对象创建速度远超回收能力,ZGC 同样会 Allocation Stall,表现为应用线程卡在 try_allocate 上。
元空间(Metaspace)回收几乎不靠“GC 触发”,而靠“类卸载条件”
很多人以为 Full GC 会顺手清理元空间,其实不会。元空间回收只发生在满足全部三个条件时:
- 该类的所有实例已被回收
- 加载该类的
ClassLoader实例本身已不可达(比如 Web 应用热部署后旧 ClassLoader 被弃用) - 该类的
Class对象没被任何地方强引用(包括java.lang.instrument或 JMX 注册表)
所以生产环境常见现象是:元空间持续增长直到 java.lang.OutOfMemoryError: Metaspace,但堆内存充足、GC 日志里却看不到元空间回收动作——这时候该查的是类加载器泄漏,不是调 -XX:MaxMetaspaceSize。


















