JVM 垃圾回收(GC)原理与实战调优
一、GC 核心目标与回收范围
1.1 核心目标
垃圾回收 GC 本质是 JVM 提供的自动内存管理机制,核心解决两大问题,在吞吐量与业务响应延迟之间寻找平衡点:
- 识别垃圾对象:自动识别堆内存中程序不再访问的对象,释放无效内存占用,无需开发者手动写代码释放内存;
- 高效执行回收:以最小的业务停顿代价回收垃圾、整理内存碎片,防止内存持续上涨触发 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,无法回收
0Main.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 类:
- 虚拟机栈帧局部变量表中引用的对象(方法内局部变量);
- 方法区静态属性引用对象,
static静态变量; - 方法区常量引用对象,
static final常量; - 本地方法栈 JNI 引用对象;
- 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 经典分代收集器组合
分代收集器必须新生代 + 老年代成对搭配
表格
| 新生代收集器 | 老年代收集器 | 特点 | 适用场景 |
|---|---|---|---|
| Serial | Serial Old | 单线程 GC,全程 STW,开销低,回收慢 | 小内存客户端程序,堆小于 1G |
| ParNew | CMS | 新生代并行,老年代并发回收,STW 短,低延迟优先 | Web、微服务,堆 4G 以内,接口对延迟敏感 |
| Parallel Scavenge | Parallel Old | 并行 GC,吞吐量优先,JDK8 默认收集器 | 后台批任务、报表计算,不关心接口延迟 |
4.2 G1 收集器(JDK9 默认,企业大内存首选)
G1(Garbage-First)不再严格隔离新生代老年代,属于整堆收集器,适合堆内存大于 4G、追求可控停顿的业务。
- 堆内存拆分成大量大小统一的 Region 块(1~32M),Region 可以动态充当 Eden、Survivor、老年代、大对象 Humongous 区;
- 垃圾优先策略:优先回收垃圾占比最高的 Region,单位时间回收内存最多;
- 可预测停顿:支持设置预期最大 GC 停顿时间
-XX:MaxGCPauseMillis; - 回收采用标记复制,不会产生内存碎片。
G1 回收阶段
- YoungGC:回收 Eden、Survivor,多线程 STW;
- MixedGC:G1 核心阶段,同时回收新生代 + 部分老年代 Region,停顿可控;
- FullGC:保底机制,单线程 STW,性能极差,线上必须尽量避免触发。
4.3 收集器选型参考
- 堆内存<1G:Serial GC,简单低开销
- 追求吞吐量、后台批任务:Parallel GC(JDK8 默认)
- 低延迟、堆<4G:ParNew + CMS
- 低延迟、堆>4G:G1(企业项目首选)
- JDK9 及以上版本:直接使用默认 G1,绝大多数业务无需修改
五、对象生命周期与 GC 触发流程(JDK8 Parallel GC)
- 对象分配:新对象优先分配在 Eden;超大对象直接分配到老年代,避免新生代反复 GC 复制大对象;
- Minor GC 触发:Eden 填满,触发 Minor GC,仅清理新生代;标记复制算法,Eden+S0 存活对象复制到 S1,清空 Eden 和 S0,交换 S0/S1 角色;
- 对象年龄晋升:每熬过一次 Minor GC,对象年龄 + 1;到达阈值(默认 15),晋升老年代;
- 动态晋升规则:Survivor 区里,同年龄对象总大小超过 Survivor 一半,该年龄及以上全部晋升老年代,不需要等到 15;
- Full GC 触发:老年代空间不足、Minor GC 存活对象无法晋升到老年代,触发 Full GC,整堆回收(新生代 + 老年代 + 元空间),STW 时间很长,生产环境要严控。
GC 名词区分(面试高频)
- Minor GC:只回收新生代,触发频繁,STW 很短,业务几乎无感;
- Major GC:只回收老年代,触发少,停顿较长;
- Full GC:全堆 + 元空间回收,STW 可达秒级,线上调优核心目标就是减少 Full GC。
六、GC 调优实战:监控、参数、问题排查
GC 调优不是追求极致最优参数,核心思路:监控采集 GC 日志 → 定位瓶颈 → 少量修改参数 → 压测验证,迭代优化,匹配业务 SLA 即可,拒绝过度调优。
6.1 调优核心原则
- 首要目标:减少 Full GC,Full GC 是业务卡顿最大来源;
- 控制 Minor GC 频率,高频 Minor GC 累积 STW,同样影响性能;
- 业务优先:延迟敏感业务选低延迟收集器,离线任务优先吞吐量;
- 小步改动:每次只修改 1~2 项参数,方便定位改动带来的影响;
- 满足业务指标即可,不用盲目追求理论极限。
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 老年代碎片多,有空闲内存但无法分配大对象,触发 FullGC | 1. 开启 CMS 压缩;2. 直接迁移 G1,从根源解决碎片 |
| G1 发生 FullGC | MixedGC 回收不及时,调低InitiatingHeapOccupancyPercent阈值;减少大对象创建 |
6.5 内存泄漏定位
内存泄漏是对象长期持有引用无法被 GC 回收,内存持续上涨,频繁 FullGC 最终 OOM。 常见泄漏场景
- static 静态集合持续 add 对象,没有清理逻辑;
- 单例对象持有短生命周期业务对象引用;
- IO、数据库连接、线程池资源使用完成未关闭;
- ThreadLocal 使用完毕不执行 remove,线程池复用线程导致对象无法释放。
定位步骤
- jmap 导出堆快照 heap.hprof;
- MAT 工具打开,查看 Leak Suspects 泄漏可疑报告;
- 通过支配树 Dominator Tree,找出占用内存最高对象;
- 追踪对象引用链,定位代码里未释放引用的地方,修改业务代码。
七、总结
GC 本质是 JVM 自动内存管理,无法消除 GC,只能控制 GC 带来的影响。
- 垃圾判定依靠 GC Roots 可达性分析,四种引用类型控制回收策略;
- 分代收集基于对象生命周期差异,新生代标记复制,老年代使用标记清除 / 整理;
- 收集器选型看堆大小和业务目标:追求吞吐量选 Parallel,低延迟大内存场景优先 G1;
- GC 调优标准流程:开启 GC 日志、持续监控、定位瓶颈、少量调整参数验证;调优的最终目标,是让 GC 停顿、频率满足业务 SLA,保障业务稳定运行。
所属版权:JSON工具 JSON工具
原文路径:
转载声明:转载时必须以链接形式注明原始出处及本声明。