JVM 诊断与性能
约 1233 字大约 4 分钟
布欧-Lewyon
2026-05-17
线上问题排查是区分中高级工程师的关键能力。本节覆盖 JVM 诊断工具链、JIT 编译与逃逸分析等性能概念,以及微基准测试的注意事项。
JVM 诊断工具链
jcmd —— 多功能诊断
JDK 8+ 推荐优先使用 jcmd 替代 jstack、jmap、jinfo 等分离工具:
# 查看所有 Java 进程
jcmd -l
# 打印线程堆栈
jcmd <PID> Thread.print
# 打印堆信息
jcmd <PID> GC.heap_info
# 查看 JVM 启动参数
jcmd <PID> VM.flags
# 查看系统属性
jcmd <PID> VM.system_properties
# dump 线程到文件
jcmd <PID> Thread.print -l > /tmp/threads.txt线程堆栈分析
堆栈文件通常很大,关注以下内容:
# 死锁检测(jstack/jcmd 会自动报告)
Found one Java-level deadlock
# 大量 BLOCKED 线程 — 锁竞争激烈
java.lang.Thread.State: BLOCKED (on object monitor)
# 大量 WAITING 线程 — 线程池或连接池耗尽
java.lang.Thread.State: WAITING (parking)
# 长时间 RUNNABLE — 可能 CPU 密集或死循环
java.lang.Thread.State: RUNNABLE堆转储与 OOM 分析
# 主动 dump(生产慎用,会暂停应用)
jcmd <PID> GC.heap_dump /tmp/heap.hprof
# JVM 参数:OOM 时自动 dump
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/tmp/heap.hprof分析工具:Eclipse MAT(Memory Analyzer Tool)或 JProfiler。关注:
- 最大的对象(
Biggest Objects)。 - GC root 引用链(排查泄漏——对象被谁引用导致无法回收)。
java.lang.OutOfMemoryError: Java heap space→ 堆不够,调-Xmx。java.lang.OutOfMemoryError: Metaspace→ 类加载过多,调-XX:MaxMetaspaceSize。java.lang.OutOfMemoryError: Direct buffer memory→ 直接内存泄漏。
JIT 编译入门
JIT(Just-In-Time)编译器在运行时将热点代码(频繁执行的方法)编译为本地机器码,大幅提升性能。
分层编译
解释执行 → C1(客户端编译,快速启动)→ C2(服务端编译,极致优化)- 方法调用次数到达阈值后从解释执行升级到 C1,再到 C2。
-XX:+PrintCompilation查看哪些方法被编译了。-XX:CompileThreshold调整编译阈值(默认:C1 1500 次,C2 10000 次)。
内联(Inlining)
JIT 最有效的优化之一——将小方法的调用处直接替换为方法体,消除调用开销:
// JIT 会将 add 方法内联到调用处
int result = add(a, b);
// 编译后 → int result = a + b;
private int add(int x, int y) {
return x + y;
}- 短方法(默认 < 35 字节码)、被频繁调用的方法最可能被内联。
-XX:+PrintInlining查看内联决策。
逃逸分析(Escape Analysis)
JIT 分析对象的作用域,判断是否"逃逸"出了方法或线程,从而做优化:
public long sum() {
// Point 对象只在本方法内使用,未逃逸 → 可能被分配在栈上(而非堆)
Point p = new Point(1, 2);
return p.x + p.y;
}逃逸分析带来的优化(JDK 8+ 默认开启,-XX:+DoEscapeAnalysis):
- 栈上分配:对象不逃逸时在栈上分配,随方法结束自动销毁,减少 GC 压力。
- 标量替换:将对象的字段拆解为独立的局部变量。
- 同步消除:如果
synchronized块中的对象不逃逸,可移除同步。
JMH 微基准测试
微基准测试(Microbenchmark)测量微小代码片段的性能,极易写出错误的结果(JIT 预热、死代码消除、常量折叠等陷阱)。JMH(Java Microbenchmark Harness)是官方推荐的正确工具。
Maven 依赖
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>test</scope>
</dependency>基本用法
@BenchmarkMode(Mode.Throughput) // 吞吐量(ops/s)
@Warmup(iterations = 3, time = 1) // 预热 3 轮
@Measurement(iterations = 5, time = 1) // 正式测量 5 轮
@Fork(1) // 启动一个独立进程
public class StringBenchmark {
@Benchmark
public String concatWithPlus() {
return "a" + "," + "b" + "," + "c";
}
@Benchmark
public String concatWithBuilder() {
return new StringBuilder()
.append("a").append(",")
.append("b").append(",")
.append("c").toString();
}
}mvn clean verify
java -jar target/benchmarks.jar输出示例:
Benchmark Mode Cnt Score Error Units
StringBenchmark.concatWithPlus thrpt 5 500.123 ± 8.45 ops/s
StringBenchmark.concatWithBuilder thrpt 5 480.567 ± 7.89 ops/s易错点
- 必须预热:让 JIT 完成优化后再测量。
- 防止死代码消除:
Blackhole消费结果,或从方法返回计算结果。 - Fork 隔离:不同 Benchmark 方法可能相互影响,建议
@Fork(1)。 - 不做没有意义的微基准:简单字符串拼接的差异在真实应用中几乎不可感知,优先关注算法复杂度。
小结
- 诊断工具:
jcmd(线程堆栈、堆信息)、jstack(死锁检测)、jmap/jcmd(堆转储)。 - JIT 优化:内联(短方法消除调用开销)、逃逸分析(栈上分配、同步消除)。
- 微基准用 JMH,注意预热、Blackhole 消费、Fork 隔离。
- 易错:不要在生产环境随意执行
jcmd GC.heap_dump——dump 过程中会暂停应用(Full GC)。线上排查优先用jcmd Thread.print(线程堆栈,无暂停),确认是线程问题还是内存问题后再决定是否 dump。 - 思考任务:用一个循环多次调用
StringBuilder.append和String +拼接,用 JMH 测量两者性能差异并在不同预热轮次下观察吞吐量变化。
上一节:类加载与反射
下一节:Maven 基础
