JVM垃圾回收算法与调优实战
前言
Java的自动内存管理是Java语言最核心的优势之一,而垃圾回收(Garbage Collection,GC)机制则是自动内存管理的核心。理解JVM的垃圾回收机制,对于编写高性能Java应用、排查线上问题至关重要。本文将全面解析JVM的内存模型、垃圾回收算法以及调优实战技巧。
一、JVM内存模型
1.1 运行时数据区
┌─────────────────────────────────────────────────────────────────┐
│ JVM 进程内存 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Method Area (方法区) │ │
│ │ - 类信息 │ │
│ │ - 静态变量 │ │
│ │ - 常量池 │ │
│ │ - JIT编译后的代码 │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ JDK 1.7: PermGen (有大小限制,需手动设置) │ │ │
│ │ │ JDK 1.8+: Metaspace (使用Native Memory,自动扩展) │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Heap (堆) │ │
│ │ ┌────────────────┐ ┌────────────────┐ │ │
│ │ │ Young Gen │ │ Old Gen │ │ │
│ │ │ ┌────┬────┐ │ │ │ │ │
│ │ │ │Eden│ S0 │S1 │ │ │ │ │
│ │ │ │ │ │ │ │ │ │ │
│ │ │ └────┴────┘ │ │ │ │ │
│ │ └────────────────┘ └─────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Native Method Stack │ │
│ │ (本地方法栈) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Java Stack │ │
│ │ (Java虚拟机栈) │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Thread1 │ │ Thread2 │ │ Thread3 │ │ │
│ │ │ Stack │ │ Stack │ │ Stack │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Program Counter Register │ │
│ │ (程序计数器) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
1.2 堆内存详解
// JVM堆内存配置示例
// -Xms512m 初始堆大小 512MB
// -Xmx1024m 最大堆大小 1GB
// -Xmn256m 新生代大小 256MB
// -XX:MetaspaceSize=128m 元空间初始大小
// -XX:MaxMetaspaceSize=512m 元空间最大大小
public class HeapMemoryDemo {
public static void main(String[] args) {
// 查看JVM内存信息
Runtime runtime = Runtime.getRuntime();
System.out.println("JVM总内存: " + runtime.totalMemory() / 1024 / 1024 + "MB");
System.out.println("JVM最大内存: " + runtime.maxMemory() / 1024 / 1024 + "MB");
System.out.println("空闲内存: " + runtime.freeMemory() / 1024 / 1024 + "MB");
System.out.println("可用处理器: " + runtime.availableProcessors());
}
}
1.3 对象分配流程
对象分配流程图:
┌─────────────────┐
│ 申请新内存 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 是否在TLAB分配?│
└────────┬────────┘
│
┌────────┴────────┐
│ Yes │ No
▼ ▼
┌─────────┐ ┌─────────────────┐
│TLAB分配 │ │ Eden区CAS分配 │
│(线程本地│ │ (失败则加锁) │
│缓冲区) │ └────────┬────────┘
└────┬────┘ │
│ ▼
│ ┌─────────────────┐
│ │ Eden空间足够? │
│ └────────┬────────┘
│ │
│ ┌───────┴───────┐
│ │ Yes │ No
│ ▼ ▼
│ ┌──────────┐ ┌─────────────┐
│ │ 分配成功 │ │ 触发Minor GC│
│ └──────────┘ └──────┬──────┘
│ │
│ ▼
│ ┌─────────────────┐
└──────────────────►│ GC后空间足够? │
└────────┬────────┘
│
┌───────┴───────┐
│ Yes │ No
▼ ▼
┌──────────┐ ┌─────────────┐
│ 分配成功 │ │ 触发Full GC │
└──────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ 空间仍不足? │
│ (OOM) │
└─────────────┘
二、垃圾回收算法
2.1 引用计数算法
// 引用计数算法原理
// 每个对象有一个引用计数器
// 每当有引用指向它时,计数器+1
// 每当引用失效时,计数器-1
// 计数器为0时,对象可被回收
public class ReferenceCounting {
public static void main(String[] args) {
// a引用计数 = 1
Object a = new Object();
// b引用计数 = 1
Object b = new Object();
// a引用计数 = 2
// b引用计数 = 1
Object c = a;
// a引用计数 = 1
a = null;
// b引用计数 = 0,可以回收
b = null;
// c引用计数 = 0,可以回收
c = null;
}
}
// 引用计数无法解决的问题:循环引用
class Node {
Node ref; // 引用计数永远不为0
}
Node n1 = new Node(); // n1: count=1
Node n2 = new Node(); // n2: count=1
n1.ref = n2; // n2: count=2
n2.ref = n1; // n1: count=2
n1 = null; // n1: count=1 (循环引用导致内存泄漏)
n2 = null; // n2: count=1
2.2 可达性分析算法
JVM采用可达性分析算法(根搜索算法)来判断对象是否存活:
// 可达性分析中的"GC Roots"包括:
// 1. 虚拟机栈中引用的对象
// 2. 方法区中静态属性引用的对象
// 3. 方法区中常量引用的对象
// 4. 本地方法栈中JNI引用的对象
// 5. JVM内部引用(Class对象、异常对象等)
// 6. 同步锁持有的对象
// 7. JVM的一些临时对象
public class GC Roots Demo {
// GC Root: 静态变量
private static Object staticObj = new Object();
// GC Root: 常量
private static final Object constObj = new Object();
public void method() {
// GC Root: 局部变量
Object localObj = new Object();
// GC Root: 锁对象
synchronized (localObj) {
System.out.println("locked");
}
}
}
2.3 四种引用类型
public class ReferenceTypes {
public static void main(String[] args) {
// 1. 强引用 - 即使OOM也不会回收
Object strongRef = new Object(); // 永远不会被GC
// 2. 软引用 - 内存不足时回收
SoftReference<Object> softRef = new SoftReference<>(new Object());
// 可用于缓存,内存不足时自动清理
// 3. 弱引用 - 下次GC必回收
WeakReference<Object> weakRef = new WeakReference<>(new Object());
// 适合存储元数据、ThreadLocal
// 4. 虚引用 - 随时可能被回收
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(),
new ReferenceQueue<>());
// 用于跟踪对象被回收的时间
// 5. ReferenceQueue - 监控引用状态
ReferenceQueue<Object> queue = new ReferenceQueue<>();
WeakReference<Object> trackedRef = new WeakReference<>(new Object(), queue);
// 检查对象是否被回收
Reference<? extends Object> ref = queue.poll();
if (ref != null) {
System.out.println("对象已被回收");
}
}
}
2.4 标记-清除算法
算法流程:
初始状态: 标记阶段: 清除阶段:
┌───────────┐ ┌───────────┐ ┌───────────┐
│ □ □ ■ │ │ □ □ ■ │ │ □ □ │
│ □ ■ □ │ => │ □ ■ □ │ => │ □ │
│ ■ □ □ │ │ ■ □ □ │ │ │
│ □ □ ■ │ │ □ □ ■ │ │ │
└───────────┘ └───────────┘ └───────────┘
■ = 存活对象 ■ = 标记的对象 □ = 可回收空间
□ = 可回收对象
缺点:
1. 效率不稳定(对象多时标记时间长)
2. 内存碎片化
2.5 复制算法
算法流程(新生代使用):
┌──────────────────┐ ┌──────────────────┐
│ Eden │ │ Survivor S0 │
│ (8/10) │ => │ (1/10) │
│ 存活对象 │ │ │
│ │ │ │
│ ┌──────────────┐ │ │ ┌──────────────┐ │
│ │ S1 (1/10) │ │ │ │ S2 (1/10) │ │
│ │ (To) │ │ │ │ (From) │ │
│ └──────────────┘ │ │ └──────────────┘ │
└──────────────────┘ └──────────────────┘
优点:没有内存碎片
缺点:可用内存减半
实际应用:
- 新生代 8:1:1 分区
- 存活对象少(通常<10%),复制开销小
- 适合短生命周期对象
2.6 标记-整理算法
算法流程(老年代使用):
标记阶段: 整理阶段: 清理阶段:
┌───────────┐ ┌───────────┐ ┌───────────┐
│ ■ □ ■ │ │ ■ ■ ■ │ │ ■ ■ ■ │
│ □ □ ■ │ => │ │ => │ │
│ ■ □ □ │ │ │ │ │
│ □ □ ■ │ │ │ │ │
└───────────┘ └───────────┘ └───────────┘
优点:
1. 没有内存碎片
2. 内存连续
缺点:
1. 移动对象需要更新引用
2. STOP THE WORLD 时间长
2.7 分代收集算法
// JVM分代策略
//
// 新生代 (Young Generation)
// ├── Eden区 (80%)
// ├── Survivor S0 (10%)
// └── Survivor S1 (10%)
// - 回收频率:频繁
// - 算法:复制算法
// - Minor GC
//
// 老年代 (Old/Tenured Generation)
// - 回收频率:较低
// - 算法:标记-清除/标记-整理
// - Full GC / Major GC
//
// 元数据区 (Metaspace)
// - 存储类信息、常量、JIT编译代码
// - Full GC时可能回收
public class GCGenerationDemo {
public static void main(String[] args) {
// 这些对象会分配在 Eden 区
List<Object> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
list.add(new byte[1024 * 1024]); // 1MB
}
// 如果Survivor区装不下,进入老年代
// 大对象直接进入老年代 (-XX:PretenureSizeThreshold)
// 长期存活的对象进入老年代
// -XX:MaxTenuringThreshold=15 (默认)
}
}
三、垃圾收集器
3.1 收集器关系图
┌─────────────────────────────────────────────────────────────────┐
│ Young Generation │
│ │
│ ┌─────────┐ ┌────────────┐ ┌───────────┐ ┌─────────┐ │
│ │ Serial │──►│ ParNew │──►│ Parallel │──►│ G1 │ │
│ │ (STW) │ │ (并行) │ │ Scavenge │ │ (并行) │ │
│ └─────────┘ └────────────┘ └───────────┘ └─────────┘ │
│ │ │ │
│ └────────────┐ │ │
│ ┌─▼─────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────┐ │
│ │ Old Generation │ │
│ ├───────────────────────┤ │
│ │ Serial Old (MSC) │ │
│ │ Parallel Old │ │
│ │ CMS │ │
│ │ G1 (并发) │ │
│ │ ZGC (并发) │ │
│ │ Shenandoah │ │
│ └───────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
3.2 Serial收集器
# 配置 Serial 收集器
-XX:+UseSerialGC
# 特点:
# - 单线程收集
# - STW (Stop The World) 时间长
# - 简单高效,没有线程开销
# - 适用于Client模式、小内存应用
3.3 Parallel收集器
# 配置 Parallel 收集器
-XX:+UseParallelGC # 新生代
-XX:+UseParallelOldGC # 老年代
# 关键参数
-XX:ParallelGCThreads=4 # 并行GC线程数
-XX:MaxGCPauseMillis=200 # 最大GC停顿时间(目标)
-XX:GCTimeRatio=19 # GC时间占比 = 1/(1+GCTimeRatio)
# 特点:
# - 多线程并行收集
# - 吞吐量优先
# - 适用于后台批处理任务
3.4 CMS收集器
# 配置 CMS 收集器
-XX:+UseConcMarkSweepGC
# 收集阶段:
# 1. 初始标记 (Initial Mark) - STW, 标记GC Roots
# 2. 并发标记 (Concurrent Mark) - 并发追踪引用链
# 3. 重新标记 (Remark) - STW, 修正并发标记期间的变动
# 4. 并发清除 (Concurrent Sweep) - 并发清除垃圾
# 关键参数
-XX:CMSInitiatingOccupancyFraction=68 # 老年代占用率触发CMS
-XX:+UseCMSCompactAtFullCollection # Full GC时整理内存
-XX:CMSFullGCsBeforeCompaction=5 # 多少次Full GC后整理
# 缺点:
# - CPU敏感(与用户线程并发)
# - 无法处理浮动垃圾
# - 内存碎片
3.5 G1收集器
# 配置 G1 收集器
-XX:+UseG1GC
# 特点:
# - 面向服务端应用的收集器
# - 将堆划分为多个大小相等的Region
# - 可以设置最大停顿时间
# - 支持并发标记
# - 整理空闲时间可控
# 关键参数
-XX:MaxGCPauseMillis=200 # 最大停顿时间目标
-XX:G1HeapRegionSize=1~32MB # Region大小,必须是2的幂
-XX:InitiatingHeapOccupancyPercent=45 # 触发GC的堆占用率
# 收集阶段:
# 1. Young GC - 收集年轻代Region
# 2. Mixed GC - 收集年轻代+老年代
# 3. Full GC - (单线程或Parallel)
3.6 ZGC收集器
# 配置 ZGC 收集器 (JDK 11+)
-XX:+UseZGC
# 特点:
# - 停顿时间 < 10ms
# - 停顿时间与堆大小无关
# - 支持TB级堆内存
# - 并发收集
# - 着色指针技术
# 关键参数
-XX:MaxGCPauseMillis=2 # 最大停顿时间(亚毫秒级)
-XX:+UseZGC # 启用ZGC
四、GC调优实战
4.1 GC日志分析
# 开启GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/path/to/gc.log
-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=5
-XX:GCLogFileSize=10M
# 日志解读
[GC (Allocation Failure) [ParNew: 6144K->512K(6144K), 0.0125663 secs] 8192K->1024K(16384K), 0.0128034 secs] [Times: user=0.05 sys=0.01, real=0.01 secs]
# 解释:
# Allocation Failure - 分配失败触发Minor GC
# ParNew - 新生代收集器
# 6144K->512K(6144K) - Eden区 6144K->512K, 总容量6144K
# 8192K->1024K(16384K) - 堆内存 8192K->1024K, 总容量16384K
# 0.0128034 secs - 停顿时间
4.2 常见GC问题与解决方案
// 问题1:频繁Minor GC
// 原因:Eden区太小,对象分配频繁
// 解决:增大Eden区
// -XX:NewSize=512m -XX:MaxNewSize=512m
// 问题2:频繁Full GC
// 原因:大对象、老年代碎片化、Metaspace不足
// 解决:
// -XX:PretenureSizeThreshold=1024*1024 (大对象直接进老年代)
// -XX:+UseCMSCompactAtFullCollection
// -XX:MetaspaceSize=256m
// 问题3:GC停顿时间长
// 原因:老年代空间不足、大对象分配
// 解决:选择低延迟收集器
// -XX:+UseZGC 或 -XX:+UseShenandoahGC
// 问题4:OOM
// 排查步骤:
// 1. jmap -heap <pid> 查看堆使用情况
// 2. jmap -dump:format=b,file=heap.hprof <pid> 导出堆dump
// 3. MAT/JProfiler 分析内存占用
4.3 完整调优配置示例
# 通用服务器配置
JAVA_OPTS="
-server
-Xms4g -Xmx4g # 堆大小
-Xmn2g # 新生代
-XX:MetaspaceSize=256m
-XX:MaxMetaspaceSize=512m
# G1收集器配置
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200
-XX:G1HeapRegionSize=4m
-XX:InitiatingHeapOccupancyPercent=45
-XX:G1ReservePercent=10
# GC日志
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:/var/log/app-gc.log
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heap.hprof
# OOM时打印详细信息
-XX:+PrintCommandLineFlags
-XX:+PrintTenuringDistribution
"
4.4 Arthas排查工具
# 使用Arthas排查GC问题
# 1. 查看JVM内存
dashboard
# 2. 查看GC情况
jvm | grep -i gc
# 3. 生成堆dump
heapdump /path/to/heap.hprof
# 4. 查看对象占用
ognl '@java.lang.Runtime@getRuntime().totalMemory()'
# 5. 监控GC频率
monitor -c 5 com.xxx.MyClass method
总结
JVM垃圾回收是Java工程师必须掌握的核心知识:
- 理解内存模型:堆分为新生代和老年代,合理的分代是GC的基础
- 掌握核心算法:标记-清除、复制、标记-整理各有优劣
- 选择合适的收集器:吞吐量优先选Parallel,低延迟选G1/ZGC
- 学会分析GC日志:通过日志发现问题
- 实战调优:根据业务特点调整参数
GC调优没有银弹,需要根据具体业务场景和监控数据来调整。