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 调优流程是:
- 监控 JVM 状态。
- 分析问题类型。
- 明确调优目标。
- 制定参数或代码方案。
- 灰度验证。
- 压测或线上观察。
- 固化配置和复盘结论。
每次只改一个主要变量,否则很难判断到底是哪项改动产生了效果。
常用 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
如果线程处于 BLOCKED、WAITING、TIMED_WAITING,要结合堆栈看它在等什么资源。
第三方工具
JVM 排查常用的第三方工具包括:
| 工具 | 作用 |
|---|---|
| GCEasy | 在线分析 GC 日志 |
| MAT | 分析 heap dump,定位内存泄漏 |
| GCViewer | 本地查看 GC 日志图表 |
| Arthas | 在线诊断 Java 应用 |
Arthas 很适合线上排查,例如查看类从哪个 jar 加载、方法耗时、线程状态、JVM 实时信息、生成火焰图等。
JVM 参数分类
JVM 参数大致分三类:
| 类型 | 示例 | 说明 |
|---|---|---|
| 标准参数 | -version、-classpath | 各 JVM 实现基本都支持 |
| 非标准参数 | -Xms、-Xmx、-Xss | HotSpot 常用,但不属于标准规范 |
| 不稳定参数 | -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 定位。
排查步骤:
- 找到 Java 进程 ID。
- 导出线程栈。
- 搜索
deadlock或查看互相等待的锁。 - 找到对应业务代码。
- 调整加锁顺序或减少锁范围。
死锁问题通常不是 JVM 参数能解决的,而是代码锁设计问题。
小结
JVM 调优可以记住三句话:
- 没有监控,不谈调优。
- 没有目标,不改参数。
- 没有验证,不算完成。
工具负责告诉我们 JVM 正在发生什么;参数只是手段;真正的调优能力,是根据证据判断问题在堆、栈、GC、类加载还是线程。