强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 记录的事件类型

事件类别包含内容
GCGC 暂停时间、堆变化、收集器类型
CPUCPU 负载、线程执行时间
内存对象分配、内存池使用
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观察方法入参
线上 Debugwatch + trace无需断点调试
火焰图profilerCPU 热点分析

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

⚠️ 注意事项

  1. 不要盲目调优 — 先用工具确认瓶颈在哪里,避免"过早优化"。
  2. Xms = Xmx — 生产环境初始堆和最大堆设为一样,避免动态扩展带来的停顿。
  3. 不要设置过大堆 — GC 暂停时间与堆大小正相关;堆越大,Full GC 暂停越长。
  4. 元空间取代了永久代 — JDK 8+,类元数据存放在本地内存,不在堆中。
  5. 线上诊断工具慎用 — trace/watch 等命令有性能开销,仅在排查时使用。

💡 技巧

  1. GC 日志分析GCEasy 在线分析 GC 日志,直观图表。
  2. 火焰图 — Arthas profilerasync-profiler 生成 CPU 火焰图。
  3. JFR 持续记录 — 生产环境始终开启低开销的 JFR 记录,出问题时直接分析。
  4. 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 定位两个线程互相等待锁。

📖 扩展阅读