2026年7月11日 · 5 分钟阅读

JVM 调优实战:JDK 工具、GC 日志、参数配置与问题定位

梳理 JVM 调优常用工具、JVM 参数、GC 日志分析、堆内存与元空间优化、垃圾收集器选择、OOM 和死锁定位。

JVM 调优不是背参数。真正的调优过程应该是:先监控,再判断,再制定方案,最后验证结果。

如果没有指标和日志,直接改 -Xmx、换垃圾收集器,很容易把问题变得更隐蔽。

JVM 调优什么时候做

不是所有 Java 应用都需要 JVM 调优。只有当问题指向内存、GC、线程或运行时状态时,才应该进入 JVM 层。

常见信号包括:

  • 系统吞吐量下降。
  • 响应时间变长,尤其是 P99 抖动。
  • 老年代持续上涨。
  • Full GC 频繁。
  • GC 停顿超过业务可接受范围。
  • 出现 OutOfMemoryError
  • 本地缓存占用大量内存。
  • 线程数异常上涨或出现死锁。

JVM 调优的核心是两件事:合理分配内存,高效回收垃圾。

调优步骤

比较稳的 JVM 调优流程是:

  1. 监控 JVM 状态。
  2. 分析问题类型。
  3. 明确调优目标。
  4. 制定参数或代码方案。
  5. 灰度验证。
  6. 压测或线上观察。
  7. 固化配置和复盘结论。

每次只改一个主要变量,否则很难判断到底是哪项改动产生了效果。

常用 JDK 工具

JDK 自带了很多排查工具,先掌握这些,线上定位能力就会稳很多。

工具作用
jps查看 Java 进程
jstat查看 GC、类加载、JIT 等运行状态
jinfo查看或调整 JVM 参数
jmap查看堆信息,生成堆转储
jhat分析 heap dump,较老,实际常用 MAT 替代
jstack生成线程栈快照,排查死锁和线程阻塞
VisualVM图形化监控 JVM

jps

jps 类似 Linux 的 ps,但只列出 Java 进程。

常用命令:

jps
jps -l
jps -m
jps -v

定位 JVM 问题时,第一步通常就是先拿到进程 ID。

jstat

jstat 可以查看 JVM 运行时统计信息,尤其适合观察 GC。

示例:

jstat -gc <pid> 1000 10

含义是每隔 1 秒采样一次,共采样 10 次。

常见字段包括 Eden、Survivor、老年代容量和使用量,以及 Young GC、Full GC 次数和耗时。它适合快速判断 GC 是否频繁、老年代是否持续增长。

jmap

jmap 常用于查看堆内存和生成 dump。

常见用法:

jmap -heap <pid>
jmap -histo <pid>
jmap -dump:format=b,file=heap.hprof <pid>

线上生成 dump 要谨慎,因为大堆转储可能导致应用停顿,并产生很大的文件。

jstack

jstack 用于生成线程快照,适合排查:

  • 死锁。
  • 线程阻塞。
  • CPU 飙高时的热点线程。
  • 大量线程等待同一把锁。

示例:

jstack <pid> > thread-dump.txt

如果线程处于 BLOCKEDWAITINGTIMED_WAITING,要结合堆栈看它在等什么资源。

第三方工具

JVM 排查常用的第三方工具包括:

工具作用
GCEasy在线分析 GC 日志
MAT分析 heap dump,定位内存泄漏
GCViewer本地查看 GC 日志图表
Arthas在线诊断 Java 应用

Arthas 很适合线上排查,例如查看类从哪个 jar 加载、方法耗时、线程状态、JVM 实时信息、生成火焰图等。

JVM 参数分类

JVM 参数大致分三类:

类型示例说明
标准参数-version-classpath各 JVM 实现基本都支持
非标准参数-Xms-Xmx-XssHotSpot 常用,但不属于标准规范
不稳定参数-XX:+UseG1GC高级参数,不同版本可能变化

常见内存参数:

-Xms    堆初始大小
-Xmx    堆最大大小
-Xmn    新生代大小
-Xss    单线程栈大小

堆不是越大越好。堆太小会频繁 GC,堆太大可能带来更长停顿。

GC 日志参数

JDK 8 常见 GC 日志参数:

-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
-Xloggc:./logs/gc.log

GC 日志要重点看:

  • GC 类型。
  • 触发原因。
  • 回收前后内存变化。
  • GC 耗时。
  • Full GC 次数和间隔。
  • 老年代是否持续上涨。

常见触发原因包括:

  • Allocation Failure:新生代空间不足。
  • Metadata GC Threshold:元空间达到阈值。
  • System.gc():代码或框架显式触发。

堆内存与元空间优化

堆内存优化先看对象分配和存活情况。

如果老年代持续上涨,要考虑:

  • 是否有集合长期持有对象。
  • 本地缓存是否没有淘汰策略。
  • 是否存在监听器、线程、连接未释放。
  • 是否有大对象频繁创建。

元空间上涨则重点看类加载:

  • 是否动态生成大量类。
  • 是否频繁热部署。
  • 是否类加载器无法卸载。

垃圾收集器选择

收集器选择要看目标。

目标常见选择
吞吐量优先Parallel Scavenge + Parallel Old
响应时间优先ParNew + CMS
大多数服务端应用G1
超低延迟、大堆ZGC

G1 常见配置思路:

-XX:+UseG1GC
-Xmx4g
-XX:MaxGCPauseMillis=200

MaxGCPauseMillis 是目标,不是保证。停顿越短,垃圾收集器付出的额外成本通常越高。

OOM 定位思路

遇到 OOM,先确认是哪种 OOM:

  • Java heap space:堆空间不足。
  • Metaspace:元空间不足。
  • unable to create new native thread:线程或系统资源不足。
  • Direct buffer memory:直接内存不足。

建议开启:

-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=./logs

然后用 MAT 分析 dump,看哪些对象占用最多、谁在引用它们、是否存在不该长期存活的对象。

死锁定位思路

死锁通常用 jstack 或 Arthas 定位。

排查步骤:

  1. 找到 Java 进程 ID。
  2. 导出线程栈。
  3. 搜索 deadlock 或查看互相等待的锁。
  4. 找到对应业务代码。
  5. 调整加锁顺序或减少锁范围。

死锁问题通常不是 JVM 参数能解决的,而是代码锁设计问题。

小结

JVM 调优可以记住三句话:

  1. 没有监控,不谈调优。
  2. 没有目标,不改参数。
  3. 没有验证,不算完成。

工具负责告诉我们 JVM 正在发生什么;参数只是手段;真正的调优能力,是根据证据判断问题在堆、栈、GC、类加载还是线程。