JVM 的垃圾处理机制
一、垃圾回收的流程
JVM
的垃圾回收(Garbage Collection, GC)大体分为两步:
- 可达性分析(找到谁是垃圾)
- 垃圾回收算法(使用合适的算法处理垃圾)
二、可达性分析
可达性分析用来判断一个对象是否还存活。
JVM 会从一些 GC Roots(如虚拟机栈中的局部变量、方法区的静态变量、本地方法栈中的引用等)开始向外搜索,如果能通过引用链找到某个对象,那么该对象就是"可达"的,不会被回收;反之,就是"垃圾"。
需要注意:对象被标记为垃圾后,并不会立刻清除,而是等到下一次触发 GC 时才会被统一清理。
三、垃圾回收算法
常见的 GC 基础算法有三种:
-
标记-清除(Mark-Sweep)
- 过程:先标记所有存活对象,再清理未标记的垃圾对象。
- 缺点:会产生 内存碎片。
-
复制(Copying)
- 过程:将存活对象复制到另一块内存区域,清空原区域。
- 优点:速度快,没有碎片。
- 缺点:理论上浪费一半空间。
- 实际 JVM 中,新生代采用 Eden + 两个 Survivor 区(常见比例 8:1:1),大大提高了利用率。
-
标记-整理(Mark-Compact)
- 过程:标记存活对象后,将它们移动到一边,保持内存连续,再清理边界之外的垃圾。
- 优点:解决碎片问题。
- 缺点:移动对象需要额外开销。
四、分代收集
不同算法各有优劣,JVM 采用 分代收集(Generational GC) 的思想:
-
新生代
- 大多数对象朝生夕死。
- 使用 复制算法,快速回收。
- 新生代进一步划分为 1 个 Eden 区 和2 个 Survivor 区。
- 对象先分配在 Eden,经过多次 Minor GC 存活下来的对象晋升到老年代。
-
老年代
- 存放生命周期较长的对象。
- 使用 标记-整理算法(或标记-清除 + 整理)。
- 避免来回复制带来的性能损耗。
-
大对象
- 一些特别大的对象可能会直接分配到老年代,避免在新生代中频繁复制。
五、现代 GC 收集器
在实际 JVM 中,基础算法往往会被组合和优化,形成不同的收集器:
1. Serial GC
- 特点:单线程,GC 时 Stop-The-World。
- 算法:新生代用复制算法,老年代用标记-整理。
- 适用场景:小应用、单核 CPU。
2. Parallel GC(吞吐量优先)
- 特点:多线程并行回收,追求吞吐量最大化。
- 适用场景:后台计算、大数据处理。
- 缺点:GC 停顿仍然较长。
3. CMS(Concurrent Mark-Sweep)
- 特点:并发标记和清除,减少停顿时间。
- 算法:基于标记-清除。
- 优点:适合对延迟敏感的应用(如 Web 服务)。
- 缺点:会产生内存碎片,吞吐量低于 Parallel GC;在 JDK 9 起被标记为废弃。
4. G1(Garbage-First)
- 特点:目前主流,JDK 9 之后默认收集器。
- 设计:堆被划分为多个 Region,每次优先回收垃圾最多的 Region。
- 优点 :兼顾吞吐量和低延迟,可预测停顿时间(如
-XX:MaxGCPauseMillis=200
)。 - 算法:新生代 → 复制;老年代 → 标记-整理。
5. 其他新一代收集器
- ZGC(JDK 11+):超低延迟,GC 停顿 < 10ms,支持 TB 级堆。
- Shenandoah(JDK 12+):与 ZGC 类似,低延迟 GC。
六、小结
- 新生代:复制算法(快速回收,大部分对象很快死亡)。
- 老年代:标记-整理(减少碎片,适合长期对象)。
- 常见收集器 :
- 小应用:Serial GC
- 吞吐量优先:Parallel GC
- 低延迟:CMS(逐渐淘汰)、G1(主流)
- 超大堆/超低延迟:ZGC、Shenandoah