Java 完全指南 / 27 - JVM 调优:GC、内存、JFR、JMC、Arthas
27 - JVM 调优:GC、内存、JFR、JMC、Arthas
JVM 内存模型
┌────────────────────── JVM 内存 ──────────────────────┐
│ │
│ 堆(Heap)—— 对象实例,GC 主要管理区域 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 新生代(Young Generation) │ │
│ │ ┌────────┬────────────────┬────────────────┐ │ │
│ │ │ Eden │ Survivor 0 (S0)│ Survivor 1 (S1)│ │ │
│ │ │ 新对象 │ 存活对象 │ 空(交替使用) │ │ │
│ │ └────────┴────────────────┴────────────────┘ │ │
│ ├─────────────────────────────────────────────────┤ │
│ │ 老年代(Old Generation) │ │
│ │ 长期存活的对象、大对象 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 非堆(Non-Heap) │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 元空间(Metaspace)—— 类元数据、方法信息 │ │
│ │ 代码缓存(Code Cache)—— JIT 编译的机器码 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 线程私有 │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 虚拟机栈(VM Stack)—— 方法调用、局部变量 │ │
│ │ 本地方法栈(Native Stack)—— JNI 调用 │ │
│ │ 程序计数器(PC Register)—— 当前执行指令 │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
GC 工作原理简述
新对象 → Eden 区
│
Minor GC(复制算法)
│
┌───────┴───────┐
│ 存活对象 │ 死亡对象
▼ ▼
S0/S1 交替 回收
│
(年龄达到阈值,默认15)
▼
老年代
│
Major GC / Full GC(标记-清除/整理)
▼
回收
GC 收集器选择
| 收集器 | 算法 | 特点 | 推荐场景 |
|---|
| G1 | 分区 + 复制 | 平衡吞吐和延迟 | 默认,适合大多数应用 |
| ZGC | 着色指针 + 读屏障 | 超低延迟(<1ms) | 大堆、延迟敏感 |
| Shenandoah | 着色指针 | 低延迟 | 类似 ZGC |
| Serial | 复制 + 标记整理 | 单线程 | 小堆、嵌入式 |
| Parallel | 复制 + 标记整理 | 高吞吐 | 批处理、后台计算 |
| Epsilon | 不回收 | 零开销 | 性能测试、短生命周期 |
常用 GC 参数
# ---- G1(JDK 9+ 默认)----
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 目标最大暂停时间(ms)
-XX:G1HeapRegionSize=8m # Region 大小(1~32MB,2的幂)
-XX:InitiatingHeapOccupancyPercent=45 # 触发并发标记的堆占用率
-XX:G1NewSizePercent=5 # 新生代最小比例
-XX:G1MaxNewSizePercent=60 # 新生代最大比例
# ---- ZGC(JDK 15+ 生产就绪,JDK 21 分代 ZGC)----
-XX:+UseZGC
-XX:+ZGenerational # JDK 21 分代 ZGC(推荐开启)
# ---- 通用参数 ----
-Xms512m # 初始堆大小
-Xmx2g # 最大堆大小
-Xss512k # 线程栈大小
-XX:MetaspaceSize=256m # 元空间初始大小
-XX:MaxMetaspaceSize=512m # 元空间最大大小
-XX:ReservedCodeCacheSize=256m # 代码缓存大小
# ---- GC 日志 ----
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags:filecount=5,filesize=10m
# ---- OOM 处理 ----
-XX:+HeapDumpOnOutOfMemoryError # OOM 时自动 dump 堆
-XX:HeapDumpPath=/var/log/heapdump.hprof
-XX:OnOutOfMemoryError="kill -9 %p" # OOM 时杀死进程(配合 K8s 重启)
# ---- 性能调优 ----
-XX:+UseCompressedOops # 压缩指针(堆<32GB时默认开启)
-XX:+AlwaysPreTouch # 启动时预分配内存
-XX:+UseStringDeduplication # 字符串去重(G1 专用)
-XX:+OptimizeStringConcat # 优化字符串拼接
常用诊断工具
jps — 查看 Java 进程
jps -lv # 显示进程ID、完整类名和JVM参数
# 12345 com.example.Application -Xmx2g -XX:+UseG1GC
jstat — GC 统计
# 每 1 秒输出一次 GC 统计
jstat -gcutil <pid> 1000
# S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
# 0.00 45.23 67.89 32.14 95.43 91.23 12 0.045 1 0.120 0.165
# S0/S1: Survivor 区使用率
# E: Eden 区使用率
# O: 老年代使用率
# M: 元空间使用率
# YGC: Young GC 次数
# FGC: Full GC 次数
# 查看 GC 原因
jstat -gc <pid> 1000
jmap — 堆转储
# 生成堆转储文件
jmap -dump:format=b,file=/tmp/heap.hprof <pid>
# 只 dump 存活对象(会触发 Full GC)
jmap -dump:live,format=b,file=/tmp/heap-live.hprof <pid>
# 堆摘要信息
jmap -heap <pid>
# 对象直方图(按实例数排序)
jmap -histo <pid> | head -20
# num #instances #bytes class name
# 1: 890123 71209840 [B
# 2: 234567 28148040 java.lang.String
# 3: 89012 7120960 java.lang.Integer
jstack — 线程转储
# 打印所有线程堆栈
jstack <pid> > /tmp/threads.txt
# 检测死锁
jstack -l <pid> | grep -A 20 "deadlock"
# 线程状态统计
jstack <pid> | grep "java.lang.Thread.State" | sort | uniq -c
常见线程状态
| 状态 | 说明 | 可能原因 |
|---|
RUNNABLE | 运行中 | 正常 |
BLOCKED | 等待锁 | synchronized 竞争 |
WAITING | 无限等待 | Object.wait(), LockSupport.park() |
TIMED_WAITING | 超时等待 | Thread.sleep(), Lock.tryLock(timeout) |
NEW | 未启动 | 刚创建 |
TERMINATED | 已结束 | 执行完毕 |
CPU 高排查流程
# 1. 找到 CPU 最高的线程
top -Hp <pid>
# PID USER PR NI VIRT RES SHR S %CPU %MEM COMMAND
# 12350 app 20 0 10g 2g 12m S 98.0 25.0 java
# 2. 将线程 ID 转为十六进制
printf '%x\n' 12350
# 303e
# 3. 在 jstack 中查找该线程
jstack <pid> | grep -A 30 "nid=0x303e"
JFR(Java Flight Recorder)
JFR 是 JDK 内置的低开销性能分析工具,可以持续记录 JVM 事件。
# 启动录制(60 秒)
jcmd <pid> JFR.start duration=60s filename=/tmp/recording.jfr
# 持续录制(手动停止)
jcmd <pid> JFR.start settings=profile filename=/tmp/recording.jfr
jcmd <pid> JFR.stop
# 使用 profile 预设(包含更多信息)
jcmd <pid> JFR.start settings=profile filename=/tmp/recording.jfr duration=5m
# Java 代码中启动
jdk.jfr.Recording recording = new jdk.jfr.Recording();
recording.enable("jdk.GCHeapSummary");
recording.enable("jdk.CPULoad");
recording.start();
// ... 业务代码 ...
recording.stop();
recording.dump(Path.of("/tmp/recording.jfr"));
JFR 记录的事件类型
| 事件类别 | 包含内容 |
|---|
| GC | GC 暂停时间、堆变化、收集器类型 |
| CPU | CPU 负载、线程执行时间 |
| 内存 | 对象分配、内存池使用 |
| I/O | 文件读写、Socket 操作 |
| 线程 | 锁竞争、线程等待 |
| 编译 | JIT 编译、方法内联 |
| 类加载 | 类加载/卸载 |
JMC(JDK Mission Control)
JMC 是 JFR 文件的图形化分析工具。
# 启动 JMC
jmc
# 打开 .jfr 文件进行分析
# 可以看到:
# - CPU 使用率时间线
# - 内存分配热点
# - GC 暂停分析
# - 锁竞争热点
# - 方法级性能分析
Arthas(在线诊断神器)
Arthas 是阿里开源的 Java 诊断工具,可在线排查问题,无需重启应用。
# 安装并启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
# 选择目标进程后进入 Arthas 控制台
Arthas 常用命令
# ---- 系统概览 ----
dashboard # 仪表盘:CPU、内存、GC、线程一览
thread # 线程列表
thread -n 3 # CPU 占用最高的 3 个线程
thread --state BLOCKED # 筛选阻塞状态的线程
thread -b # 检测死锁
memory # 内存使用详情
heapdump /tmp/heap.hprof # 导出堆转储
# ---- 类和方法诊断 ----
sc *Service # 搜索类(Search Class)
sm *Service * # 搜索方法(Search Method)
jad com.example.UserService # 反编译类
watch com.example.UserService getUser '{params, returnObj}' # 观察方法入参和返回值
watch com.example.UserService getUser '{params, throwExp}' -e # 观察异常
# ---- 调用链路分析 ----
trace com.example.OrderService createOrder # 方法调用链路耗时
trace com.example.OrderService createOrder '#cost > 100' # 只显示耗时 >100ms 的
stack com.example.UserService getUser # 方法调用栈
# ---- 性能分析 ----
profiler start # 开始 CPU 火焰图采样
profiler stop --format html --file /tmp/flame.html # 生成火焰图
# ---- 表达式执行 ----
ognl '@com.example.AppConfig@getConfig("key")' # 调用静态方法
ognl '#user=new com.example.User("test"), #user.getName()' # 创建对象并调用
# ---- 热修复(线上紧急修复)----
# 1. 反编译
jad --source-only com.example.UserService > /tmp/UserService.java
# 2. 修改源码
vim /tmp/UserService.java
# 3. 编译
mc -c <classLoaderHash> /tmp/UserService.java -d /tmp/classes
# 4. 热替换
retransform /tmp/classes/com/example/UserService.class
Arthas 诊断场景
| 场景 | 命令 | 说明 |
|---|
| CPU 高 | thread -n 3 | 找到 CPU 最高的线程 |
| 内存泄漏 | heapdump + MAT | 导出堆转储分析 |
| 慢方法 | trace | 追踪方法调用耗时 |
| 锁竞争 | thread -b | 检测阻塞线程 |
| 参数校验 | watch | 观察方法入参 |
| 线上 Debug | watch + trace | 无需断点调试 |
| 火焰图 | profiler | CPU 热点分析 |
JVM 调优流程
1. 监控发现异常(CPU 高 / 内存大 / 响应慢 / OOM)
│
2. 收集信息(GC 日志 / 堆转储 / 线程转储 / JFR)
│
3. 分析根因
├── GC 频繁 → 调整堆大小、GC 策略
├── OOM → 内存泄漏分析
├── CPU 高 → 找到热点代码
└── 响应慢 → 锁竞争 / I/O 阻塞
│
4. 实施调优
│
5. 压测验证
│
6. 持续监控
GC 调优实践
场景一:Web 应用(低延迟要求)
# 目标:暂停时间 < 200ms
-Xms2g -Xmx2g # 固定堆大小
-XX:+UseG1GC
-XX:MaxGCPauseMillis=200 # 暂停目标
-XX:G1HeapRegionSize=4m # 较小的 Region
-XX:InitiatingHeapOccupancyPercent=35 # 提前触发并发标记
-XX:+ParallelRefProcEnabled # 并行引用处理
场景二:批处理(高吞吐要求)
# 目标:最大化吞吐量
-Xms8g -Xmx8g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=500 # 放宽暂停限制
-XX:G1NewSizePercent=40 # 更大的新生代
-XX:G1MaxNewSizePercent=60
场景三:大堆低延迟(ZGC)
# 目标:< 10ms 暂停,堆 > 16GB
-Xms32g -Xmx32g
-XX:+UseZGC
-XX:+ZGenerational # JDK 21 分代 ZGC
-XX:SoftMaxHeapSize=28g # 软上限
常见问题排查
OOM 排查
# 1. 确保开启了自动堆转储
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/
# 2. 使用 MAT 分析 heap dump
# - Dominator Tree: 查找占用内存最多的对象
# - Leak Suspects: 自动分析可能的泄漏点
# 3. 常见 OOM 原因
# - java.lang.OutOfMemoryError: Java heap space → 堆内存不足或泄漏
# - java.lang.OutOfMemoryError: Metaspace → 类加载泄漏(反射/动态代理过多)
# - java.lang.OutOfMemoryError: GC overhead limit exceeded → GC 耗时过长
# - java.lang.StackOverflowError → 递归过深
CPU 高排查
# 1. 找到 Java 进程
jps -lv
# 2. 找到 CPU 最高的线程
top -Hp <pid>
# 3. 转换线程 ID
printf '%x\n' <tid>
# 4. 定位堆栈
jstack <pid> | grep -A 30 "nid=0x<hex_tid>"
# 或者直接用 Arthas
# arthas-boot.jar → thread -n 5
⚠️ 注意事项
- 不要盲目调优 — 先用工具确认瓶颈在哪里,避免"过早优化"。
- Xms = Xmx — 生产环境初始堆和最大堆设为一样,避免动态扩展带来的停顿。
- 不要设置过大堆 — GC 暂停时间与堆大小正相关;堆越大,Full GC 暂停越长。
- 元空间取代了永久代 — JDK 8+,类元数据存放在本地内存,不在堆中。
- 线上诊断工具慎用 — trace/watch 等命令有性能开销,仅在排查时使用。
💡 技巧
- GC 日志分析 — GCEasy 在线分析 GC 日志,直观图表。
- 火焰图 — Arthas
profiler 或 async-profiler 生成 CPU 火焰图。 - JFR 持续记录 — 生产环境始终开启低开销的 JFR 记录,出问题时直接分析。
- Prometheus + Grafana — 使用 Micrometer 暴露 JVM 指标,实时监控。
// Spring Boot 暴露 JVM 指标
// application.yml
management:
endpoints:
web:
exposure:
include: prometheus,health
metrics:
tags:
application: myapp
🏢 业务场景
- GC 调优: 电商大促前优化 GC 参数,将 P99 延迟从 500ms 降到 100ms。
- 内存泄漏: OOM 后分析 heap dump,定位
List 无限增长的泄漏点。 - 性能瓶颈: JFR + Arthas 火焰图发现数据库查询是热点。
- 死锁排查:
jstack + thread -b 定位两个线程互相等待锁。
📖 扩展阅读