JVM垃圾回收(GC)核心原理全解析

JVM 垃圾回收(GC)核心原理全解析(HotSpot 视角,JDK 8~21 适用)

垃圾回收(Garbage Collection,简称 GC)是 JVM 内存管理的核心机制,帮助程序员自动回收无用对象内存,避免内存泄漏。JVM 的 GC 基于分代假设:大多数对象朝生夕死,少数对象长寿。

本文从垃圾判断回收算法分代收集常见收集器STW 与优化一步步解析原理,到实际调优建议。基于 HotSpot VM(最主流实现),适用于 JDK 8+(包括 ZGC 等新收集器)。

一、GC 基础:为什么需要 GC?什么是垃圾?

  • 为什么需要 GC?
    C/C++ 需要手动 malloc/free,容易内存泄漏或悬垂指针。Java 通过 GC 自动管理堆内存,让开发者专注业务。
  • 什么是垃圾?
    堆上对象如果不再被任何线程引用,就是垃圾。GC 先判断哪些是垃圾,再回收。

二、垃圾判断算法(如何判定对象已死?)

两大主流算法:引用计数 vs 可达性分析(JVM 用后者)。

算法原理优点缺点JVM 使用?
引用计数对象有一个计数器,引用+1,失效-1;计数=0 时回收简单、实时回收无法解决循环引用(A 引用 B,B 引用 A)
可达性分析从 GC Roots 开始搜索,可达的对象是活的;不可达的是垃圾解决循环引用需要 STW(暂停用户线程)是(主流)

GC Roots 是什么?(搜索起点)

  • 虚拟机栈中的局部变量引用
  • 方法区静态变量引用
  • 方法区常量引用
  • 本地方法栈 JNI 引用
  • 线程对象(Thread)
  • 已加载类对象(Class)
  • 监视器(synchronized 持有的对象)

对象“死亡”过程(不是立即回收):

  1. 不可达 → 标记为垃圾
  2. 如果重写了 finalize(),放入 F-Queue 队列,自救机会(finalize() 中重新建立引用)
  3. finalize() 只执行一次,自救失败 → 真正回收

注意:finalize() 已被弃用(JDK9+),建议用 try-with-resources 或 Cleaner。

三、垃圾回收算法(如何回收垃圾?)

三大经典算法 + 分代优化。

  1. 标记-清除(Mark-Sweep)
  • 过程:标记活对象 → 清除垃圾
  • 优点:简单
  • 缺点:碎片化(空间不连续,导致大对象分配失败);效率低(标记+清除两次扫描)
  • 适用:老年代(CMS 使用)
  1. 标记-复制(Mark-Copy)
  • 过程:标记活对象 → 复制到另一块空间 → 清空原空间
  • 优点:无碎片;简单高效(只扫描活对象)
  • 缺点:空间浪费(一半空间空闲)
  • 适用:新生代(存活率低,复制少量对象)
  1. 标记-整理(Mark-Compact)
  • 过程:标记活对象 → 整理(活对象移到一端) → 清空剩余
  • 优点:无碎片;空间利用高
  • 缺点:整理阶段慢(移动对象,更新引用)
  • 适用:老年代(Serial Old、Parallel Old 使用)

算法对比表

算法空间利用时间效率无碎片?典型收集器
标记-清除中等CMS
标记-复制Serial、ParNew
标记-整理Serial Old、G1(局部)

四、分代收集理论(Generational Collection)

基于弱代假设(大多数对象短命)和强代假设(越老越难死),堆分为新生代老年代

  • 新生代(Young Gen):Eden + 两个 Survivor(默认 8:1:1)
  • 新对象在 Eden 分配
  • Minor GC(Young GC):回收新生代(复制算法)
  • 存活对象 → Survivor(年龄+1)
  • 年龄达阈值(默认15,-XX:MaxTenuringThreshold)或 Survivor 满 → 晋升老年代
  • 老年代(Old Gen):存长寿对象
  • Major GC / Full GC:回收老年代 + 新生代(标记-清除/整理)
  • 触发:老年代满、方法区满、担保失败
  • 担保机制(Allocation Failure Guarantee):
    新生代 Minor GC 前检查老年代空间是否够容纳所有新生代对象(极端情况)。不够 → Full GC。

分代比例:新生代:老年代 = 1:2(-XX:NewRatio=2,默认)

五、常见 GC 收集器(Collector)及原理

HotSpot 有多种收集器,按新生代/老年代分类,按串行/并行/并发分类。

新生代收集器

  • Serial:单线程、复制算法、STW
  • ParNew:Serial 多线程版、并行
  • Parallel Scavenge:并行、吞吐量优先(-XX:MaxGCPauseMillis、-XX:GCTimeRatio)

老年代收集器

  • Serial Old:单线程、标记-整理
  • Parallel Old:Parallel Scavenge 的老年代版、并行
  • CMS(Concurrent Mark Sweep):并发标记-清除,低暂停
  • 过程:初始标记(STW)→ 并发标记 → 重新标记(STW)→ 并发清除
  • 优点:低延迟(并发阶段用户线程可运行)
  • 缺点:碎片、CPU 敏感、浮动垃圾(并发时新垃圾)

全堆收集器(JDK8+ 主流):

  • G1(Garbage First):分代 + 区域(Region)
  • 堆分成小 Region(1-32MB),优先回收价值高的(垃圾多)
  • 过程:Young GC(复制)+ Mixed GC(年轻+老)
  • 优点:可预测暂停(-XX:MaxGCPauseMillis=200ms,默认)
  • 缺点:高内存占用(Remembered Set 跟踪跨代引用)
  • 适用:大堆(>4GB)、低延迟
  • ZGC(JDK11+):超低暂停(<10ms)
  • 使用彩色指针(Colored Pointers)标记,不需 STW 整理
  • 优点:暂停极短、支持 TB 级堆
  • 缺点:吞吐量稍低
  • Shenandoah(JDK12+,OpenJDK):类似 ZGC,并发整理

收集器组合推荐(生产常用):

  • 小堆、低延迟:ParNew + CMS
  • 高吞吐:Parallel Scavenge + Parallel Old
  • 大堆、均衡:G1(JDK9+ 默认)
  • 超大堆、低暂停:ZGC / Shenandoah

参数示例

# G1 示例
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 -Xmx4g -Xms4g

六、STW(Stop The World)与优化

  • STW 是什么?:GC 时暂停所有用户线程,确保引用一致。
  • 为什么需要?:防止标记时引用变化。
  • 优化:并发收集器(CMS/G1/ZGC)把标记/清除并发化,只短暂停。

安全点(Safepoint):线程可暂停的位置(方法调用、循环末尾)。
安全区域:线程 sleep/block 时,整个区域安全。

七、GC 触发时机 & 日志分析

  • Minor GC:Eden 满
  • Full GC:老年代满、方法区满、System.gc()(不推荐)

日志参数:-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:gc.log

示例日志

[GC (Allocation Failure) [PSYoungGen: 2048K->256K(2560K)] 2048K->256K(9728K), 0.001s]
  • PSYoungGen:新生代
  • Allocation Failure:分配失败触发

八、快速记忆口诀 & 调优建议

  • 判断:可达分析 > 引用计数(循环问题)
  • 算法:清除碎片多、复制空间半、整理移动慢
  • 分代:年轻复制快、老年清除/整理
  • 收集器:Serial 单、Par 多、CMS 并发碎片、G1 区域预测、ZGC 彩色低停

生产调优

  1. 监控 GC 日志(jstat、jmap、VisualVM)
  2. 堆大小:-Xms = -Xmx(避免动态扩展)
  3. 新生代大小:-XX:NewSize(太大 Minor GC 少但久,小太频繁)
  4. 优先 G1/ZGC
  5. 避免大对象、内存泄漏(ThreadLocal 未 remove)

GC 是 JVM 的黑魔法,原理懂了,调优就简单了。有具体 GC 日志想分析、或某个收集器想深挖(如 G1 的 RSet),告诉我,我继续展开!

文章已创建 4424

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部