首页 / 博客 / 详情

JVM 垃圾回收(GC)原理与实战调优

作者:sa | 2026-10-20 | 浏览 24 GC原理 JVM 垃圾回收

一、GC 核心目标与回收范围

1.1 核心目标

垃圾回收 GC 本质是 JVM 提供的自动内存管理机制,核心解决两大问题,在吞吐量与业务响应延迟之间寻找平衡点:

  1. 识别垃圾对象:自动识别堆内存中程序不再访问的对象,释放无效内存占用,无需开发者手动写代码释放内存;
  2. 高效执行回收:以最小的业务停顿代价回收垃圾、整理内存碎片,防止内存持续上涨触发 OOM 内存溢出。

两个关键指标:

  • 吞吐量:业务代码运行时长占总运行时间的比例;
  • 响应时间:GC 造成的业务线程暂停(STW)时长,直接影响接口响应卡顿。

1.2 GC 回收范围界定

GC 的核心工作区域仅限 JVM 堆内存,其余内存区域不会执行常规垃圾回收:

  • 元空间(JDK8 替代永久代):存储类的元数据,仅在空间耗尽时做少量类卸载回收,不属于 GC 核心工作区;
  • 线程私有内存(虚拟机栈、本地方法栈、程序计数器):生命周期绑定线程,线程销毁时内存自动释放,GC 不参与管理。

二、垃圾判定底层原理:怎么判断对象是垃圾

垃圾对象定义:当前业务代码不存在任何有效引用链可以访问的对象。JVM 有两种判定算法,现代 JVM 全部采用可达性分析算法。

2.1 引用计数算法(历史方案,现已淘汰)

原理:每个对象维护一个引用计数器。当有新引用指向对象,计数器 + 1;引用失效置空,计数器 - 1;计数器等于 0 时,判定为垃圾对象,可以回收。 致命缺陷:无法处理循环引用。两个对象互相持有对方引用,外部没有任何引用指向这一组对象,两个对象计数器都为 1,永远不会被判定为垃圾,造成内存泄漏。

// 循环引用案例,引用计数算法无法回收
Object a = new Object();
Object b = new Object();
a.ref = b;
b.ref = a;
a = null;
b = null;
// a、b无外部引用,但互相引用,引用计数保持1,无法回收
0

Main.java:2: error: unnamed classes are a preview feature and are disabled by default. Object a = new Object(); ^ (use --enable-preview to enable unnamed classes) Main.java:4: error: class, interface, enum, or record expected a.ref = b; ^ Main.java:5: error: class, interface, enum, or record expected b.ref = a; ^ Main.java:6: error: class, interface, enum, or record expected a = null; ^ Main.java:7: error: class, interface, enum, or record expected b = null; ^ 5 errors

2.2 可达性分析算法(JVM 标准实现)

原理:以GC Roots(垃圾回收根节点)作为遍历起点,沿着对象引用链向下搜索。

  • 可以从 GC Roots 遍历到达:对象存活,不能回收;
  • 引用链断裂,无法遍历到达:判定垃圾对象,可以回收。

GC Roots 常见类型(重点) GC Roots 是堆外、JVM 保证一定存活的起始节点,最常用 3 类:

  1. 虚拟机栈帧局部变量表中引用的对象(方法内局部变量);
  2. 方法区静态属性引用对象,static静态变量;
  3. 方法区常量引用对象,static final常量;
  4. 本地方法栈 JNI 引用对象;
  5. JVM 内部基础对象(Class、系统线程、类加载器)。

2.3 Java 四种引用类型,控制 GC 回收策略

JDK1.2 之后,把对象引用按强度划分 4 个等级,不同引用的回收逻辑不同,是缓存开发的基础。

表格

引用类型说明GC 回收行为典型场景
强引用普通对象引用,Object obj = new Object()只要存在强引用,无论内存是否紧张,永远不会回收,内存耗尽直接 OOM绝大多数业务对象、实体实例
软引用SoftReference弱于强引用内存充足不回收;内存即将溢出 OOM 前,优先回收软引用对象图片、业务缓存,允许缓存丢失
弱引用WeakReference弱于软引用只要发生 GC,不管内存是否足够,直接回收对象ThreadLocal 的 key、临时轻量缓存
虚引用PhantomReference强度最低,无法获取对象实例不能通过引用拿到对象,仅用于接收对象回收通知堆外内存(DirectByteBuffer)释放监控

三、分代收集思想:GC 高效回收的核心设计

分代收集的核心依据:绝大多数对象朝生夕死,创建后短时间内就失效;只有少量对象长期存活。 根据对象存活周期,把堆内存划分不同区域,不同区域匹配对应的 GC 算法,最大化回收效率。

3.1 JDK8 堆内存分代结构

堆总大小由-Xms初始堆、-Xmx最大堆控制。

JVM 堆内存 ├─新生代(约占堆 1/3):存放新创建对象,GC 触发频繁 │ ├─Eden 区(新生代 80%):新对象优先分配在这里,绝大多数对象在此诞生消亡 │ ├─Survivor0 (S0)(10%):存活对象临时保存 │ └─Survivor1 (S1)(10%):和 S0 互斥,永远一块为空 └─老年代(约占堆 2/3):长期存活对象,GC 触发频率低

新生代核心规则 Eden 空间占满,触发 Minor GC,只回收新生代;S0、S1 永远一块空闲,用于复制存活对象;多次 GC 后依然存活的对象,会晋升到老年代。

3.2 三大基础 GC 算法,分代搭配使用

GC 算法就是垃圾回收的底层执行逻辑,不同区域选择不同算法:

表格

算法执行流程优点缺点适用区域
标记 - 清除1. 标记存活对象;2. 直接清理未标记垃圾实现简单,不需要移动对象产生大量内存碎片,大量垃圾时效率差老年代(辅助)
标记 - 复制1. 标记存活对象;2. 存活对象复制到备用空闲区域;3. 清空原区域无内存碎片,存活少场景回收速度快需要预留一块空闲内存,空间利用率下降新生代(存活对象极少)
标记 - 整理1. 标记存活对象;2. 存活对象向内存一端移动;3. 清理边界外垃圾无碎片,内存利用率高移动对象成本高,STW 停顿更长老年代(存活对象多)

总结:新生代使用标记复制;老年代以标记清除为主,碎片过多时执行标记整理。

四、GC 收集器:GC 算法的工程落地实现

收集器就是 GC 算法的具体实现,分为分代收集器(JDK8 主流)和整堆收集器(JDK9 + 默认)。

前置概念 STW(Stop The World):GC 运行时暂停全部业务用户线程。所有收集器都无法彻底消除 STW,调优目标是减少 STW 时长与次数。

  • 串行收集:单线程执行 GC,全程 STW,资源开销小,回收慢;
  • 并行收集:多线程跑 GC,STW 变短,优先保证吞吐量;
  • 并发收集:GC 线程和业务线程并行运行,STW 很短,优先降低响应延迟。

4.1 JDK8 经典分代收集器组合

分代收集器必须新生代 + 老年代成对搭配

表格

新生代收集器老年代收集器特点适用场景
SerialSerial Old单线程 GC,全程 STW,开销低,回收慢小内存客户端程序,堆小于 1G
ParNewCMS新生代并行,老年代并发回收,STW 短,低延迟优先Web、微服务,堆 4G 以内,接口对延迟敏感
Parallel ScavengeParallel Old并行 GC,吞吐量优先,JDK8 默认收集器后台批任务、报表计算,不关心接口延迟

4.2 G1 收集器(JDK9 默认,企业大内存首选)

G1(Garbage-First)不再严格隔离新生代老年代,属于整堆收集器,适合堆内存大于 4G、追求可控停顿的业务。

  1. 堆内存拆分成大量大小统一的 Region 块(1~32M),Region 可以动态充当 Eden、Survivor、老年代、大对象 Humongous 区;
  2. 垃圾优先策略:优先回收垃圾占比最高的 Region,单位时间回收内存最多;
  3. 可预测停顿:支持设置预期最大 GC 停顿时间-XX:MaxGCPauseMillis;
  4. 回收采用标记复制,不会产生内存碎片。

G1 回收阶段

  1. YoungGC:回收 Eden、Survivor,多线程 STW;
  2. MixedGC:G1 核心阶段,同时回收新生代 + 部分老年代 Region,停顿可控;
  3. FullGC:保底机制,单线程 STW,性能极差,线上必须尽量避免触发。

4.3 收集器选型参考

  • 堆内存<1G:Serial GC,简单低开销
  • 追求吞吐量、后台批任务:Parallel GC(JDK8 默认)
  • 低延迟、堆<4G:ParNew + CMS
  • 低延迟、堆>4G:G1(企业项目首选)
  • JDK9 及以上版本:直接使用默认 G1,绝大多数业务无需修改

五、对象生命周期与 GC 触发流程(JDK8 Parallel GC)

  1. 对象分配:新对象优先分配在 Eden;超大对象直接分配到老年代,避免新生代反复 GC 复制大对象;
  2. Minor GC 触发:Eden 填满,触发 Minor GC,仅清理新生代;标记复制算法,Eden+S0 存活对象复制到 S1,清空 Eden 和 S0,交换 S0/S1 角色;
  3. 对象年龄晋升:每熬过一次 Minor GC,对象年龄 + 1;到达阈值(默认 15),晋升老年代;
    • 动态晋升规则:Survivor 区里,同年龄对象总大小超过 Survivor 一半,该年龄及以上全部晋升老年代,不需要等到 15;
  4. Full GC 触发:老年代空间不足、Minor GC 存活对象无法晋升到老年代,触发 Full GC,整堆回收(新生代 + 老年代 + 元空间),STW 时间很长,生产环境要严控。

GC 名词区分(面试高频)

  • Minor GC:只回收新生代,触发频繁,STW 很短,业务几乎无感;
  • Major GC:只回收老年代,触发少,停顿较长;
  • Full GC:全堆 + 元空间回收,STW 可达秒级,线上调优核心目标就是减少 Full GC。

六、GC 调优实战:监控、参数、问题排查

GC 调优不是追求极致最优参数,核心思路:监控采集 GC 日志 → 定位瓶颈 → 少量修改参数 → 压测验证,迭代优化,匹配业务 SLA 即可,拒绝过度调优。

6.1 调优核心原则

  1. 首要目标:减少 Full GC,Full GC 是业务卡顿最大来源;
  2. 控制 Minor GC 频率,高频 Minor GC 累积 STW,同样影响性能;
  3. 业务优先:延迟敏感业务选低延迟收集器,离线任务优先吞吐量;
  4. 小步改动:每次只修改 1~2 项参数,方便定位改动带来的影响;
  5. 满足业务指标即可,不用盲目追求理论极限。

6.2 监控工具

6.2.1 JVM 内置命令(生产环境无侵入,优先使用)

# 查看java进程PID
jps -l
# 持续监控GC,pid替换为进程号,1000ms打印一次,打印10次
jstat -gc 进程PID 1000 10
# 导出堆快照hprof文件,用于分析内存泄漏
jmap -dump:format=b,file=heap.hprof 进程PID
# 查看当前JVM生效参数
jinfo -flags 进程PID

6.2.2 可视化分析工具

  • MAT:分析 hprof 堆快照,定位内存泄漏,最核心工具;
  • Arthas:阿里开源线上诊断工具,无需重启应用,实时查看 GC、线程;
  • GCViewer / GCEasy:解析 GC 日志,生成图表,直观查看 GC 停顿、频率。

6.3 常用 JVM 生产参数

基础通用参数(所有收集器通用)

# 初始堆=最大堆,关闭堆动态扩容,减少性能波动
-Xms4G -Xmx4G
# 元空间配置,防止元空间溢出
-XX:MetaspaceSize=256M -XX:MaxMetaspaceSize=512M
# 新生代Eden:S0:S1比例8:1:1,默认一般无需改动
-XX:SurvivorRatio=8
# 对象晋升老年代年龄上限
-XX:MaxTenuringThreshold=15

G1 收集器配置(JDK8 手动开启,堆 > 4G 推荐)

-XX:+UseG1GC
# 目标最大停顿时间,根据业务调整,示例200ms
-XX:MaxGCPauseMillis=200
# GC并行线程数,一般匹配CPU核心数
-XX:ParallelGCThreads=8
# GC并发标记线程,通常并行线程的一半
-XX:ConcGCThreads=4
# 老年代占用达到45%触发MixedGC,可降低提前回收
-XX:InitiatingHeapOccupancyPercent=30

GC 日志与 OOM 自动转储(线上必配)

# 输出GC详情、时间戳
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps
# GC日志路径
-Xloggc:/usr/local/app/gc.log
# 日志文件轮转,防止单文件过大,保留5份,单文件最大100M
-XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=100M
# OOM自动导出堆快照,排查内存溢出必备
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/app/heap.hprof
# 禁止代码手动调用System.gc(),避免人为触发FullGC
-XX:+DisableExplicitGC

6.4 线上常见 GC 故障和解决方案

表格

故障现象处理方案
Minor GC 频繁触发,YGCT 累计高1. 调大新生代内存;2. 代码优化,减少循环内临时对象创建
Full GC 频繁,接口卡顿严重1. 增大堆内存;2.MAT 分析堆快照排查内存泄漏;3. 堆大于 4G 切换 G1 收集器
CMS 老年代碎片多,有空闲内存但无法分配大对象,触发 FullGC1. 开启 CMS 压缩;2. 直接迁移 G1,从根源解决碎片
G1 发生 FullGCMixedGC 回收不及时,调低InitiatingHeapOccupancyPercent阈值;减少大对象创建

6.5 内存泄漏定位

内存泄漏是对象长期持有引用无法被 GC 回收,内存持续上涨,频繁 FullGC 最终 OOM。 常见泄漏场景

  1. static 静态集合持续 add 对象,没有清理逻辑;
  2. 单例对象持有短生命周期业务对象引用;
  3. IO、数据库连接、线程池资源使用完成未关闭;
  4. ThreadLocal 使用完毕不执行 remove,线程池复用线程导致对象无法释放。

定位步骤

  1. jmap 导出堆快照 heap.hprof;
  2. MAT 工具打开,查看 Leak Suspects 泄漏可疑报告;
  3. 通过支配树 Dominator Tree,找出占用内存最高对象;
  4. 追踪对象引用链,定位代码里未释放引用的地方,修改业务代码。

七、总结

GC 本质是 JVM 自动内存管理,无法消除 GC,只能控制 GC 带来的影响。

  1. 垃圾判定依靠 GC Roots 可达性分析,四种引用类型控制回收策略;
  2. 分代收集基于对象生命周期差异,新生代标记复制,老年代使用标记清除 / 整理;
  3. 收集器选型看堆大小和业务目标:追求吞吐量选 Parallel,低延迟大内存场景优先 G1;
  4. GC 调优标准流程:开启 GC 日志、持续监控、定位瓶颈、少量调整参数验证;调优的最终目标,是让 GC 停顿、频率满足业务 SLA,保障业务稳定运行。
上一篇
数据库场景网闸选型采购
下一篇
Linux sudo 命令完整指南:基础操作、进阶技巧与生产安全落地