性能监控与调优
前面三章我们学了 JVM 内存模型、GC、类加载——这是”理论知识”。这一章是”动手实战”——用工具观察运行中的 JVM,排查 OOM、CPU 飙高、卡顿等真实问题。这是 Java 工程师”诊断能力”的核心——会写代码是基础,能定位问题才是高级。
这一章把生产环境最常用的工具和排查思路一次讲完。
一、JDK 自带命令行工具
JDK 自带一套命令行工具,在 $JAVA_HOME/bin 下。它们小而精,无需额外安装,是排查问题的”第一响应”工具。
1.1 jps:列出 Java 进程
jps(JVM Process Status Tool)列出当前机器上的 Java 进程,类似 ps 但只看 Java:
$ jps
12345 Main
23456 org.eclipse.jetty.start.Main
34567 sun.tools.jps.Jps
$ jps -lvm # 完整包名 + main 参数 + JVM 参数
12345 com.example.Main --port=8080 -Xmx2g -XX:+UseG1GC
-l 显示完整包名,-v 显示 JVM 参数,-m 显示 main 方法参数。
1.2 jstat:GC 统计
jstat(JVM Statistics Monitoring Tool)监控 GC、类加载、编译等统计——最常用的是看 GC 情况:
# 每 1 秒输出一次, 共 10 次, 看 GC 情况
$ jstat -gcutil 12345 1000 10
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 85.42 73.12 45.67 92.34 88.12 23 0.234 2 0.456 0.690
...
# 各列含义:
# S0/S1: Survivor 0/1 使用率
# E: Eden 使用率
# O: Old 使用率
# M: Metaspace 使用率
# CCS: 压缩类空间使用率
# YGC: Young GC 次数
# YGCT: Young GC 累计耗时
# FGC: Full GC 次数
# FGCT: Full GC 累计耗时
# GCT: 总 GC 耗时
-gcutil 看使用率,-gc 看字节数,-gc_capacity 看容量,-gccause 看 GC 原因。
1.3 jstack:线程堆栈
jstack(JVM Stack Tool)打印 JVM 的所有线程堆栈——排查死锁、CPU 飙高、卡顿的利器:
$ jstack 12345 > thread_dump.txt
# 找死锁
$ jstack -l 12345 | grep -A 20 "Found .* deadlock"
# 强制打印 (进程无响应时)
$ jstack -F 12345
jstack 输出每个线程的:
- 线程名、优先级、线程 ID(nid,十六进制)。
- 线程状态——
RUNNABLE、BLOCKED、WAITING、TIMED_WAITING。 - 调用栈——当前执行的 Java 方法栈。
"http-nio-8080-exec-3" #25 daemon prio=5 os_prio=0 tid=0x... nid=0x... waiting on condition
java.lang.Thread.State: WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
- parking to wait for <0x...> (a java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject)
at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await(AbstractQueuedSynchronizer.java:2039)
at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)
at org.apache.tomcat.util.threads.TaskQueue.take(TaskQueue.java:103)
...
找 CPU 飙高的元凶
CPU 飙高排查的标准流程(Linux):
# 1. 找占 CPU 最高的 Java 进程
$ top -c
# 假设 PID = 12345
# 2. 找该进程占 CPU 最高的线程
$ top -Hp 12345
# 假设线程 PID = 12378
# 3. 线程 PID 转十六进制
$ printf "%x\\n" 12378
# 0x305a
# 4. 在 jstack 输出里找 nid=0x305a 的线程
$ jstack 12345 | grep "nid=0x305a" -A 30
那个线程的调用栈就是 CPU 飙高的代码——可能是死循环、正则灾难、加密算法、GC 线程等。
1.4 jmap:内存堆 dump
jmap(JVM Memory Map)打印堆内存使用、生成堆 dump 文件:
# 堆内存概要
$ jmap -heap 12345
# 直方图: 各类对象数量和大小 (按大小排)
$ jmap -histo 12345 | head -20
num #instances #bytes class name
1: 1234567 123456789 [B (byte 数组)
2: 234567 23456789 java.lang.String
3: 123456 9876543 java.util.HashMap$Node
...
# 只看活对象 (会触发 Full GC, 谨慎!)
$ jmap -histo:live 12345 | head -20
# 生成 heap dump (生产环境用 -dump:format=b,file=...)
$ jmap -dump:format=b,file=heap.hprof 12345
jmap -histo 是定位内存泄漏的第一步——看哪些类的对象数量异常多。-histo:live 会触发 Full GC 只看活对象,生产慎用——会 STW 一段时间。
1.5 jcmd:万能工具
jcmd(JVM Command)是 JDK 8 引入的”统一命令行工具”,能干前面所有工具的事,还支持更多:
$ jcmd # 列出所有 Java 进程
$ jcmd 12345 help # 列出该进程支持的所有命令
$ jcmd 12345 Thread.print # 等价于 jstack
$ jcmd 12345 GC.class_stats # 类元数据统计
$ jcmd 12345 GC.heap_info # 堆信息
$ jcmd 12345 VM.flags # JVM 参数
$ jcmd 12345 VM.system_properties # 系统属性
$ jcmd 12345 GC.heap_dump heap.hprof # 堆 dump
$ jcmd 12345 JFR.start duration=60s filename=recording.jfr # 启动 JFR
jcmd 是现代推荐的工具——统一接口,比分散的 jps/jstat/jstack/jmap 更强大。
二、JConsole 与 VisualVM
2.1 JConsole
JConsole(Java Monitoring and Management Console)是 JDK 自带的 GUI 工具,监控 JVM 的内存、线程、类、GC、MBean。
$ jconsole # 启动, 选择要连接的进程
功能:
- 内存——堆/非堆/各内存池的实时使用曲线。
- 线程——线程数实时曲线,每个线程的状态和栈,死锁检测(“检测到死锁”按钮)。
- 类——已加载/卸载的类数量曲线。
- VM 摘要——JVM 参数、运行时间、CPU 占用。
- MBean——查 MBean 属性、调 MBean 操作。
适合开发调试——直观的曲线图,但生产环境少用(GUI 耗资源、远程连接需配置 JMX)。
2.2 VisualVM
VisualVM(All-in-One Java Troubleshooting Tool)比 JConsole 更强大——集成多个功能:
- 概述——JVM 信息、参数、系统属性。
- 监视——CPU、内存、线程、类的实时曲线。
- 线程——线程时间线,能看到线程何时 BLOCKED/WAITING。
- 抽样器——CPU/内存抽样,看哪个方法占 CPU 多。
- Profiler——更精细的 CPU/内存剖析(侵入式,会影响性能)。
- 快照——保存线程 dump、堆 dump。
$ jvisualvm # JDK 9 之前自带
# JDK 9+ 单独下载: https://visualvm.github.io/
VisualVM 可以打开 .hprof 文件做离线分析——内存泄漏排查的标配。
三、JFR 与 JMC
3.1 JFR(Java Flight Recorder)
JFR(Java Flight Recorder)是 Oracle 的”黑匣子”——持续记录 JVM 运行数据,开销极低(<1%),可在生产环境长期开启。JDK 11 开源(之前是商业特性)。
JFR 记录的事件包括:
- GC 事件——每次 GC 的详细信息。
- 方法采样——哪些方法执行得多(CPU profile)。
- 锁事件——锁竞争、阻塞时间。
- IO 事件——文件、socket IO。
- 类加载、异常、线程 等。
# 启动时开启 JFR, 录制 60 秒到文件
$ java -XX:StartFlightRecording=duration=60s,filename=recording.jfr MyApp
# 运行中启动
$ jcmd 12345 JFR.start duration=60s filename=recording.jfr
# 查看录制
$ jcmd 12345 JFR.check
JFR 是”低开销、连续监控”的方案——比 jstack/jmap 的”快照式”诊断更适合生产长期运行。
3.2 JMC(JDK Mission Control)
JMC(JDK Mission Control)是 JFR 的 GUI 分析工具——打开 .jfr 文件,看曲线、火焰图、热点方法、GC 详情。比 VisualVM 更专业,是 Oracle 推荐的生产级分析工具。
# 单独下载: https://www.oracle.com/java/technologies/jdk-mission-control.html
$ jmc # 启动, 打开 .jfr 文件
JMC 7+ 支持自动分析——它会指出”哪些事件可能有问题”,比如”GC 暂停过长”、“锁等待过多”。
四、async-profiler 与火焰图
async-profiler 是社区开源的低开销 profiler——比 JFR 更适合 CPU 火焰图分析。它基于 perf 或异步信号,开销极低,能精确到 JVM 内部调用。
4.1 安装
# macOS
$ brew install async-profiler
# 或下载: https://github.com/async-profiler/async-profiler/releases
4.2 CPU 火焰图
# 采样 30 秒, 输出火焰图
$ asprof -d 30 -f flame.html 12345
# 或者
$ asprof -e cpu -d 30 -f flame.html 12345
打开 flame.html——一张火焰图,横轴是方法调用栈的层级,纵轴是采样次数(CPU 占用)。宽的方法就是 CPU 热点。
4.3 内存分配火焰图
$ asprof -e alloc -d 30 -f alloc.html 12345
看哪些方法分配对象多——定位”内存压力”的源头。
火焰图是排查 CPU 性能问题最直观的工具——比看 jstack 抽样高效得多。Brendan Gregg 的火焰图理念被广泛采用。
五、常见 JVM 参数
5.1 内存参数
| 参数 | 作用 | 示例 |
|---|---|---|
-Xms | 堆初始大小 | -Xms4g |
-Xmx | 堆最大大小 | -Xmx8g |
-Xmn | 新生代大小 | -Xmn2g |
-Xss | 线程栈大小 | -Xss512k |
-XX:MetaspaceSize | 元空间初始 | -XX:MetaspaceSize=256m |
-XX:MaxMetaspaceSize | 元空间最大 | -XX:MaxMetaspaceSize=512m |
-XX:MaxDirectMemorySize | 直接内存最大 | -XX:MaxDirectMemorySize=1g |
-XX:NewRatio | Old:Young | -XX:NewRatio=2 |
-XX:SurvivorRatio | Eden:S | -XX:SurvivorRatio=8 |
5.2 GC 参数
| 参数 | 作用 |
|---|---|
-XX:+UseSerialGC | Serial GC |
-XX:+UseParallelGC | Parallel GC (JDK 8 默认) |
-XX:+UseG1GC | G1 (JDK 9+ 默认) |
-XX:+UseZGC | ZGC |
-XX:+UseShenandoahGC | Shenandoah |
-XX:MaxGCPauseMillis | 目标 GC 暂停 |
-XX:+PrintGCDetails | 打印 GC 详情 (JDK 8) |
-Xlog:gc* | GC 日志 (JDK 9+) |
-XX:+HeapDumpOnOutOfMemoryError | OOM 自动 dump |
-XX:HeapDumpPath | dump 路径 |
5.3 生产环境推荐配置
java \
-Xms4g -Xmx4g \ # 堆大小相同, 避免动态扩展
-XX:MetaspaceSize=256m \
-XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC \ # G1 收集器
-XX:MaxGCPauseMillis=200 \ # 目标 200ms 暂停
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/dumps/ \
-Xlog:gc*:file=/var/log/gc.log:time,uptime,level,tags \
-jar app.jar
六、OOM 排查实战
6.1 OOM 的类型
OOM 不是只有”堆溢出”——不同区域溢出表现不同:
| OOM 信息 | 原因 | 排查方向 |
|---|---|---|
java.lang.OutOfMemoryError: Java heap space | 堆溢出 | 内存泄漏 or 堆太小 |
GC overhead limit exceeded | GC 回收不动,98% 时间在 GC | 同上 |
Metaspace | 元空间溢出 | 动态类生成太多(CGLIB/Groovy) |
Direct buffer memory | 直接内存溢出 | NIO/Netty 泄漏 |
unable to create new native thread | 线程数过多 | 线程泄漏,调 -Xss 或 OS 限制 |
StackOverflowError | 栈溢出 | 递归过深 |
6.2 排查步骤
# 1. 拿到 heap dump (推荐启动时加 -XX:+HeapDumpOnOutOfMemoryError)
$ jmap -dump:format=b,file=heap.hprof 12345
# 2. 用 MAT (Eclipse Memory Analyzer) 打开
# 下载: https://www.eclipse.org/mat/
# 3. MAT 的关键报告
# - Leak Suspects Report 泄漏嫌疑报告
# - Dominator Tree 支配树, 看哪个对象"占住"了最多内存
# - Histogram 按类看对象数和大小
# - Path to GC Roots 看对象为什么没被 GC (找到引用链)
MAT 的Leak Suspects会自动分析,指出”哪个对象 retain 了 X MB 内存”——通常直接定位泄漏点。
6.3 常见泄漏模式
- 静态集合——
static Map一直加不删。 - ThreadLocal——线程池里 ThreadLocal 不 remove,泄漏到线程销毁。
- 监听器/回调——注册了没反注册。
- 缓存无淘汰——自己写的 HashMap 缓存没有 LRU/过期。
- 资源未关闭——IO/Connection 没 close。
- 内部类持有外部类——非静态内部类持有外部引用,外部类无法回收。
七、CPU 飙高排查实战
# 1. top 找占 CPU 高的 Java 进程
$ top
# PID = 12345, CPU 200%
# 2. top -Hp 找该进程的"罪魁线程"
$ top -Hp 12345
# 线程 PID = 12378, 占 200%
# 3. 线程 PID 转十六进制
$ printf "%x\\n" 12378
305a
# 4. jstack 找 nid=0x305a 的线程
$ jstack 12345 > td.txt
$ grep "nid=0x305a" -A 30 td.txt
那个线程的调用栈就是 CPU 飙高的代码——常见原因:
- 死循环——
while(true)没退出条件。 - 正则灾难——
Pattern在热路径反复编译,或灾难性回溯。 - 序列化/反序列化——大对象 JSON/Gson。
- GC 线程——GC 频繁,CPU 都在 GC。
- 加密/压缩——AES、ZIP 大数据。
- Stream 并行——
parallelStream在 CPU 密集任务上抢核。
八、实战:演示工具用法
下面的例子演示用 Java 代码触发不同问题(OOM、CPU 飙高、死锁),并展示如何用 MXBean 监控。
观察重点:
MemoryPoolMXBean列出各内存池——Eden、Survivor、Old、Metaspace 实际使用。ThreadMXBean.findDeadlockedThreads()返回 null 表示无死锁——程序化检测死锁。com.sun.management.OperatingSystemMXBean能拿到 JVM/系统 CPU 占用——是 JConsole 的数据源。RuntimeMXBean.getInputArguments()列出 JVM 启动参数——看实际生效的配置。leakMap只加不删——这就是典型泄漏模式,生产环境 MAT 能定位到这种static Map。
九、调优思路总结
9.1 调优三原则
- 不要过早调优——先正确,再性能。先有监控,再调优。
- 基于数据调优——基于 jstat/jfr/profiler 的数据,不靠猜。
- 一次调一个参数——同时改多个看不出哪个有效。
9.2 常见场景调优
| 场景 | 调优方向 |
|---|---|
| 频繁 Minor GC | 加大 -Xmn 或新生代比例 |
| Full GC 多 | 加大堆、查内存泄漏、换 G1/ZGC |
| GC 暂停长 | 换 G1(MaxGCPauseMillis)、ZGC |
| OOM | dump 分析,找泄漏点 |
| CPU 100% | jstack 找热线程,看是否死循环/GC 线程 |
| 元空间溢出 | 加大 -XX:MaxMetaspaceSize,查动态类生成 |
| 直接内存溢出 | 查 NIO/Netty 的 DirectByteBuffer 泄漏 |
| 启动慢 | 减小 MetaspaceSize,CDS/AppCDS |
9.3 监控建设
生产环境必须建监控:
- Prometheus + Grafana——metrics 长期监控,JVM exporter 采集 JVM 指标。
- ELK——GC 日志、应用日志集中分析。
- APM(SkyWalking/Pinpoint)——链路追踪,定位慢调用。
- JFR 长期录制——低开销,出问题能回看。
监控是调优的前提——“没监控的调优”是瞎猜。
十、本章小结
| 工具 | 用途 |
|---|---|
| jps | 列 Java 进程 |
| jstat | GC/内存统计 |
| jstack | 线程堆栈、死锁检测 |
| jmap | 堆 dump、对象直方图 |
| jcmd | 万能工具,统一接口 |
| JConsole | GUI 监控(开发用) |
| VisualVM | GUI 分析(打开 hprof) |
| JFR | 低开销连续录制(生产可用) |
| JMC | JFR 分析 GUI |
| async-profiler | 火焰图 profiler |
| MAT | heap dump 分析 |
排查思路速查:
| 问题 | 工具链 |
|---|---|
| OOM | jmap -dump → MAT → Leak Suspects |
| CPU 飙高 | top -Hp → printf %x → jstack 找 nid |
| 死锁 | jstack -l 或 JConsole |
| GC 频繁 | jstat -gcutil 看 YGC/FGC |
| 卡顿 | JFR 录制,看 GC 事件 |
| 方法热点 | async-profiler 火焰图 |
记忆口诀:
- jps 找进程,jstat 看 GC,jstack 看线程,jmap 看 heap——四件套。
- jcmd 是万能钥匙——一个工具干所有事。
- OOM 用 MAT——Leak Suspects 自动定位泄漏。
- CPU 用火焰图——async-profiler 一张图找到热点。
- JFR 是黑匣子——生产可长期开,<1% 开销。
- 调优靠数据不靠猜——先监控、再分析、再调参。
结语:调优是”诊断学”
性能调优像医生看病——症状(卡顿/OOM)、化验(jstack/jmap/JFR)、诊断(MAT/火焰图)、治疗(调参/改代码)。会写代码是”会做手术”,会调优才是”会看病”——后者更稀缺。
这一章我们看了工具和思路。下一章——第九阶段最后一章——讲 字节码基础。前面四章都是在”使用层”看 JVM,下一章钻进 .class 文件,看字节码指令到底长什么样,i++ 和 ++i 在字节码层面有什么区别,泛型擦除在字节码上是什么样子,try-with-resources 编译后是什么——把 JVM 的”最后一层神秘”揭开。我们下一章见。