news 2026/10/21 1:51:35

Java性能优化02:线上 Java 性能问题排查总纲,覆盖 CPU、线程、内存与 GC

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java性能优化02:线上 Java 性能问题排查总纲,覆盖 CPU、线程、内存与 GC

Java性能优化02:线上 Java 性能问题排查总纲,覆盖 CPU、线程、内存与 GC

Java 性能优化系列第 2 篇。本文解决一个实际问题:线上 Java 服务突然变慢时,第一步看什么,第二步用什么工具,怎样避免还没找到原因就先把现场破坏掉。

“接口慢了”只是现象,不是原因。

同一个现象,可能是代码把 CPU 跑满,也可能是线程在等数据库;可能是对象分配太快导致 GC 频繁,也可能是下游接口变慢。此时直接扩大线程池、增加堆内存或修改 GC 参数,就像没检查便先吃药:偶尔碰巧有效,更常见的是掩盖问题或制造新问题。

这篇文章不追求一次讲透所有工具,而是先建立一条稳定的排查路线:

  1. 先确认影响范围和异常时间。
  2. 从操作系统或容器判断哪类资源异常。
  3. 进入 JVM(Java 虚拟机)查看堆、线程和 JFR 证据。JFR(Java Flight Recorder)是 JDK 自带的运行记录工具,可以把一段时间内的 CPU、GC、锁和 I/O(磁盘、网络等输入输出)事件保存下来。
  4. 检查数据库、缓存、下游接口等外部依赖。
  5. 形成一个可以验证的猜测,再决定改哪里。

一、先保业务,再保现场

线上故障的第一目标不是找出最漂亮的根因,而是控制用户影响。

如果错误率还在快速上升,可以先回滚刚发布的版本、限制入口流量、摘除异常实例或临时扩容。采取这些动作时,要记录准确时间。后面只有把监控、日志和变更时间对齐,才能判断哪个动作真正有效。

只要业务情况允许,在重启进程前尽量保存下面的信息:

要保存什么为什么
异常开始和恢复时间用来对齐 CPU、GC、日志与下游监控
受影响接口、实例和用户范围判断是单实例、单机房还是整个服务的问题
最近发布、配置和流量变化性能问题经常由变化触发,但“刚改过”还不能直接证明因果
CPU、内存、磁盘和网络快照判断先查计算、内存还是 I/O 等待
线程转储和短时 JFR保存 JVM 当时正在执行或等待什么

不要一上来就做 Heap Dump(堆转储,即把整个 Java 堆写入文件)。堆很大时,它可能暂停应用、产生大文件并占满磁盘。先用低风险证据缩小范围,确认确实需要分析对象分布或泄漏时再做。

二、一张图记住排查路线

这张图最重要的不是工具名称,而是“现象 -> 证据 -> 工具”的顺序。

  • CPU 高,先确认哪个进程和线程在消耗 CPU,再找热点方法。
  • 延迟高但 CPU 不高,优先检查线程、连接池和外部依赖是否在等待。
  • 内存上涨,要区分 Java 堆、堆外内存和操作系统看到的进程内存。
  • GC 频繁,要先查分配速率和对象存活,而不是先换收集器。
  • 偶发抖动,要把 CPU、GC、锁和 I/O 放到同一时间轴上观察。

这里有一条很实用的原则:先使用能排除大类问题的便宜证据,再使用更重的工具。


三、第一层:先看操作系统或容器

JVM 运行在操作系统之上。主机 CPU 被其他进程占满、容器被限流、磁盘很慢或者网络连接堆积时,只看 Java 堆没有意义。

Linux 主机先看这些

uptimetop-H-p<PID>pidstat-p<PID>1pidstat-t-p<PID>1vmstat1iostat-xz1ss-s

这些命令分别帮助回答:

  • 整台机器的负载是否异常?
  • 是哪个 Java 进程、哪个操作系统线程在消耗 CPU?
  • CPU 时间花在用户态还是内核态?用户态通常对应应用代码执行,内核态通常对应系统调用和内核工作。
  • 是否频繁换页、磁盘等待或网络连接异常?

PID是进程编号。top -H -p <PID>会继续展开这个进程里的操作系统线程,但它仍然只说明“谁消耗资源”,不能直接说明是哪段 Java 代码。

容器里不要只看宿主机总量

dockerstats --no-stream<容器名>dockertop<容器名>-eopid,tid,pcpu,pmem,stat,comm

容器可能只有 1 核 CPU 和 512 MB 内存,而宿主机还有大量空闲资源。此时宿主机看起来很轻松,容器内部却已经到上限。

还要注意:容器内外看到的 PID 可能不同。执行jcmd时,最省事的做法是在同一个容器、同一个用户和同一个进程命名空间中运行 JDK 工具。

怎么根据系统指标选方向

看到的现象优先方向还不能直接得出的结论
Java 进程 CPU 持续高热线程、JFR、CPU 火焰图不能只凭 CPU 高就说是死循环
CPU 不高,接口仍然很慢线程等待、连接池、数据库和下游不能直接增加线程池
进程内存 RSS 持续涨Java 堆、堆外内存、线程栈、映射文件不能把 RSS 全部当作 Java 堆
磁盘等待高日志、临时文件、数据库或容器存储不能只调 JVM 参数
网络重传或连接大量堆积网络、连接池、超时和下游容量不能只看本服务代码

RSS 可以简单理解为操作系统当前放在物理内存中的进程内存。它包含 Java 堆,但不只包含 Java 堆。


四、第二层:用 jcmd 给 JVM 拍快照

jcmd是 JDK 自带的 JVM 诊断命令。它能查询版本、启动参数、堆概况、线程、JFR 和本地内存,不需要给应用增加依赖。

先找 Java 进程:

jcmd-l

再采集第一批信息:

jcmd<PID>VM.version jcmd<PID>VM.command_line jcmd<PID>VM.flags jcmd<PID>VM.uptime jcmd<PID>GC.heap_info jcmd<PID>Thread.print-ljcmd<PID>VM.native_memory summaryscale=MB

不要只把输出保存下来,还要知道每条命令回答什么:

命令回答的问题
VM.version当前进程到底使用哪个 JDK,而不是登录终端默认的 Java 版本
VM.command_line、VM.flags堆大小、GC 和启动参数是否与预期一致
VM.uptime进程是否刚重启,当前数据是否受启动和预热影响
GC.heap_info当前使用什么收集器,堆和 Metaspace 大致用了多少
Thread.print -l平台线程正在执行、等待或竞争什么
VM.native_memoryJVM 管理的堆外内存主要分布在哪里

NMT 是 Native Memory Tracking,也就是 JVM 本地内存跟踪。它必须在进程启动时开启:

-XX:NativeMemoryTracking=summary

如果线上进程没有预先开启,jcmd会明确提示 NMT 未启用,不能在故障发生后临时补出之前的数据。NMT 本身也不是整个进程内存的完整账本,第三方本地库和某些映射内存还需要结合 RSS 等系统指标判断。

JDK 21 虚拟线程怎样导出

传统平台线程可以先用Thread.print -l。如果应用使用了大量 JDK 21 虚拟线程,建议导出专门的线程文件:

jcmd<PID>Thread.dump_to_file-format=json-overwrite/tmp/threads.json

JSON 格式更适合大量虚拟线程。线程很多时,这条命令的成本也会增加,不要高频连续执行。


五、第三层:线程状态要结合调用栈看

Thread Dump(线程转储)是某一时刻所有线程调用栈的快照。常见状态可以先这样理解:

状态说人话排查重点
RUNNABLE正在运行,或者可以运行是否反复停在同一个业务方法;再与线程 CPU 或 JFR 对照
BLOCKED等待进入synchronized保护的区域锁被谁持有,是否出现大量线程等同一把锁
WAITING没有超时时间地等待某个条件是正常空闲,还是在等连接、任务结果或下游响应
TIMED_WAITING等待一段指定时间sleep、带超时等待、连接或队列轮询是否符合预期

看到大量WAITING不一定有问题。空闲线程池本来就应该等待任务。真正值得关注的是:请求已经排队或超时,而多次线程转储中的大量业务线程仍反复停在相同位置。

同样,RUNNABLE也不等于一定在疯狂使用 CPU。线程可能正在本地方法或网络调用中。要确认 CPU 热点,需要把操作系统线程 CPU、线程调用栈和 JFR 或 async-profiler 放在一起看。

建议在异常仍持续时,每隔 5 到 10 秒采集一次,连续采集 3 次。一个瞬间只能提出猜测,多份相似快照才更有说服力。


六、第四层:别忘了数据库、缓存和下游接口

Java 服务自身 CPU 和 GC 都正常,接口仍然可能很慢。

例如线程栈长期停在下面这些位置:

  • 获取数据库连接:可能是连接池用满,也可能是数据库响应慢。
  • Socket 读取:可能在等下游响应,而不是本机 CPU 不够。
  • Redis 客户端:可能是缓存服务、网络或连接池问题。
  • 文件读写:可能是磁盘、日志或共享存储变慢。

这时至少要对齐四组数据:

  1. 本接口的请求量、p95/p99 和错误率。
  2. 数据库、Redis、HTTP 客户端连接池的活跃数与等待数。
  3. 下游接口在相同时间段的延迟和错误率。
  4. 超时、重试和熔断是否让一次请求放大成多次调用。

线程栈只能告诉你“线程正在等哪里”,不能单独证明“对方服务就是根因”。还要用下游监控、链路追踪或对照实验补齐证据。


七、五类常见现象应该怎样查

1. CPU 持续高

排查顺序:

  1. 用top、pidstat或容器指标确认确实是目标 Java 进程。
  2. 找出高 CPU 线程,并记录线程编号。
  3. 在线程转储中查对应调用栈,或者直接录制短时 JFR。
  4. 用 JFR/JMC 或 async-profiler 查看热点方法。
  5. 结合代码和对照实验确认是否为算法、序列化、日志、正则或锁自旋等问题。火焰图会把采样到的调用栈按占比画成不同宽度,越宽通常代表占用的 CPU 时间越多。

Linux 的线程编号通常是十进制,线程转储中的nid常显示为十六进制,可以这样转换:

printf'%x\n'<线程编号>

第 4 篇会完整演示从热线程和火焰图定位 CPU 热点。本篇只负责把排查入口找对。

2. 延迟高,但 CPU 不高

优先问三个问题:

  • 请求是否在线程池或队列里排队?
  • 线程是否在等锁、数据库连接或下游接口?
  • 超时和重试是否设置合理?

此时盲目增加线程可能让数据库和下游承受更多并发,最终错误率更高。先看多份线程转储、连接池等待和下游延迟,再决定是否调整并发模型。

3. 内存持续上涨

先区分三个概念:

  • Java 堆:普通 Java 对象主要存放的位置。
  • Metaspace:类元数据使用的本地内存。
  • 堆外内存:线程栈、直接缓冲区、JIT 代码、本地库等使用的内存。

一次GC.heap_info只能看到当时的快照。判断泄漏要观察多次 GC 后的低点是否仍持续上升。确定是堆内对象问题后,再考虑类直方图或 Heap Dump;如果 Java 堆稳定但 RSS 上涨,应继续看 NMT、直接内存、线程数和本地库。

第 5 篇会专门演示内存分配和泄漏排查。

4. GC 频繁或停顿长

至少要看:

  • GC 类型、频率和每次停顿时间。
  • GC 前后堆占用下降了多少。
  • 对象分配速率是否过高。
  • 老年代在 GC 后是否仍持续上涨。
  • 长延迟是否真的与 GC 时间重合。

频繁但很短的 Young GC(主要回收短命对象)不一定是故障;一次长时间 Full GC(对整个堆影响更大)则可能直接影响 p99。不要只数 GC 次数,更不要看到 GC 就先增加堆内存。第 6 篇会从 GC 日志和 JFR 继续查到根因。

5. 偶发延迟抖动

偶发问题最怕各看各的时间范围。应用日志是 10:05,监控截图却是 10:20,就无法组成证据链。

JFR 的优势是能在同一时间轴记录 CPU 采样、GC、线程等待、锁竞争、文件和 Socket I/O。结合请求指标和链路追踪,才能判断延迟峰值到底与什么同时发生。


八、用脚本一次保存第一批现场

仓库提供了scripts/collect-jvm-diagnostics.sh。它只依赖 JDK 21 自带的jcmd和jfr:

shscripts/collect-jvm-diagnostics.sh<PID>results/diagnostics30

三个参数分别是 Java 进程号、输出目录和 JFR 录制秒数。脚本会保存:

  • JVM 版本、命令行、重要参数和运行时间。
  • 堆概况和线程转储。
  • NMT 摘要;没有预先开启时保留失败提示。
  • 一段settings=profile的 JFR。
  • JFR 事件摘要、CPU 负载、热点方法和 GC 停顿汇总。

脚本默认不会生成 Heap Dump,也不会执行类直方图。原因不是这两个工具没用,而是它们在大堆和高负载场景中可能产生明显停顿与磁盘压力,应该在确认必要性后单独执行。

settings=profile比 JFR 默认配置记录更多事件。线上首次使用时应先在同版本、相近负载环境验证开销,并把录制时间控制在 30 到 60 秒。不要因为 JFR 通常开销较低,就把“低开销”误解成“零开销”。


九、一次真实诊断实验

下面不是编造的故障描述,而是使用本系列项目实际采集的数据。

实验环境

项目配置
采集时间2026-10-14
应用java-performance-lab的三个接口
JavaEclipse Temurin 21.0.9 LTS
JVMG1,-Xms256m -Xmx256m,开启 NMT summary
运行方式Windows 11 上的 Linux 容器
诊断负载16 个虚拟用户,持续 35 秒,report参数为 350,000
JFRsettings=profile,采集 15 秒

负载脚本是load-tests/diagnostics.js。结果如下:

请求数吞吐量平均延迟p95p99错误率
17,346494.50 req/s35.33 ms103.46 ms118.40 ms0%

这组数据只用于制造可重复的诊断现场,不代表生产容量,也不用于和第 1 篇基线比较,因为负载模型和report数据规模都不同。

第一步:jcmd 看到了什么

快照显示:

  • 使用 G1,总堆为 256 MB,采集时使用约 162.6 MB。
  • NMT 显示 JVM 共预留 1,823 MB 地址空间,但实际提交约 531 MB。
  • 93 个线程预留约 92 MB 栈空间,当时实际提交约 8 MB。
  • 一个请求线程处于RUNNABLE,调用栈停在ReportService.isPrime。

这里最容易犯的错,是看到reserved=1823MB就说进程已经占用了 1.8 GB。reserved是预留地址空间,判断实际压力应继续看committed和操作系统 RSS。

另一份线程栈显示报价请求在HttpClient.send等待下游结果。这说明同一个进程里可以同时存在 CPU 计算和 I/O 等待,不能看到一个等待线程就把整个问题归为网络。

本次实验的Thread Dump 节选、NMT 摘要和堆概况都保存在仓库中。

第二步:JFR 能否印证 CPU 猜测

15 秒 JFR 给出三组关键证据:

  1. JVM 用户态 CPU 平均为 71.94%,最高为 77.02%。
  2. ReportService.isPrime占 51.97% 的执行采样,generate占 47.39%。
  3. 这段时间只有 1 次 GC 停顿,最长 5.57 ms。

线程转储和 JFR 独立指向同一段质数计算代码,而 GC 停顿很短。当前最合理的判断是:这个实验场景的主要矛盾是 CPU 计算,不是 GC。

这仍然不是“优化已经完成”。下一步应该针对质数计算做代码级基准和对照实验,再用相同负载验证吞吐、延迟和业务结果。第 3 篇会先解决测试是否可信,第 4 篇再完成 CPU 热点优化。

仓库中保留了JFR 热点摘要、CPU 摘要、GC 停顿摘要和k6 原始摘要。大体积.jfr文件不提交仓库,可以用采集脚本重新生成。


十、工具怎么选

工具不是越多越好。选择标准是:它能不能回答当前问题,产生的风险是否可接受。

工具最适合回答什么使用提醒
top、pidstat、vmstat、iostat、ss主机或进程的 CPU、内存、磁盘、网络是否异常先确认容器限制和 PID 映射
jcmdJVM 版本、参数、堆概况、线程、JFR、NMT必须使用 JDK 工具,并有权限连接目标进程
JFRCPU、分配、GC、锁、线程与 I/O 的时间线线上先短时录制,验证配置开销
JDK Mission Control(JMC)在图形界面中离线分析.jfr文件适合查看事件时间线和调用树,不必装在生产机
async-profilerCPU、分配和锁热点,生成火焰图Linux 权限和内核设置可能限制采样,先在测试环境验证
Arthas运行时查看线程、方法耗时和调用路径trace、watch等命令可能增加开销,范围必须收窄
Eclipse MAT离线分析 Heap Dump 和对象引用关系Heap Dump 的生成成本通常高于离线分析本身
NMTJVM 管理的本地内存分类必须启动时开启,reserved 不等于实际占用

一个稳妥的常见组合是:

  1. 系统或容器指标判断大方向。
  2. jcmd保存基本现场和多份线程转储。
  3. JFR 记录 30 到 60 秒时间线。
  4. 仍无法确认 CPU、分配或锁热点时,再使用 async-profiler。
  5. 需要在线观察具体方法时,谨慎使用 Arthas。

十一、哪些排查动作要格外谨慎

Heap Dump

文件大小通常接近实际堆使用量,生成时可能触发停顿。执行前先确认磁盘空间、实例是否可摘流量、文件如何安全转移以及是否包含敏感业务数据。

类直方图

jcmd <PID> GC.class_histogram能按类统计对象,但某些用法会带来明显停顿。先用 JFR 分配采样或已有监控判断是否真的需要。

强制 GC

不要为了“看看回收后还有多少”就在线上执行jcmd <PID> GC.run。它会主动触发 GC,既可能制造延迟,又改变你想观察的现场。

Arthastrace和watch

动态增强大量方法、输出大对象或匹配高频调用,可能明显增加 CPU 和日志量。必须限制类、方法、条件和执行次数。

高频线程转储

一秒采集几十次不会让证据更可靠,反而增加进程压力和分析噪声。通常间隔 5 到 10 秒的 3 份快照已经能看出主要趋势。


十二、把排查结果写成证据链

一次可以复盘的排查记录,至少包含下面内容:

- 异常时间和影响范围: - 最近发生的发布、配置或流量变化: - 系统或容器资源现象: - JVM 版本、启动参数和运行时间: - 线程转储中的重复调用栈: - JFR、GC 日志或剖析工具发现: - 数据库、缓存和下游同时间段表现: - 当前假设,以及哪些证据支持或反对它: - 临时止损动作和结果: - 下一步对照实验:

请注意“哪些证据反对它”这一项。只收集支持自己猜测的证据,很容易把相关性误写成因果性。


小结

线上 Java 服务变慢时,可以先按下面的顺序处理:

  1. 控制用户影响,记录所有止损动作的准确时间。
  2. 保存异常时间、范围、最近变更和流量变化。
  3. 从操作系统或容器确认 CPU、内存、磁盘和网络大方向。
  4. 用jcmd保存 JVM 参数、堆概况和多份线程转储。
  5. 用短时 JFR 把 CPU、GC、锁和 I/O 放到同一时间轴。
  6. 检查数据库、缓存、下游和连接池,不把等待问题误判成计算问题。
  7. 让不同证据相互印证,再决定改代码、参数还是架构。
  8. 谨慎使用 Heap Dump、强制 GC 和动态方法跟踪等高成本动作。

下一篇《Java性能优化:让测试结果可信,JMH、压测与常见陷阱》会回答:为什么同一段代码第一次和第十次速度不同,为什么错误的 JMH 写法会得到完全相反的结论,以及接口压测怎样避免“测得很准,但测错了东西”。


系列导航:上一篇:Java性能优化:性能优化不是调参数,先建立目标、指标与优化闭环 | 系列目录 | 下一篇:Java性能优化:让测试结果可信,JMH、压测与常见陷阱(待写)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 1:51:14

ModuleNotFoundError: No module named ‘skimage‘ 的排查与修复指南

先说结论&#xff1a;ModuleNotFoundError: No module named skimage这个报错&#xff0c;绝大多数情况下并不是 pip install 命令本身在执行过程中弹出来的&#xff0c;而是代码运行到from skimage import ...这一句时&#xff0c;Python 解释器在已安装的模块目录里翻了个底朝…

作者头像 李华
网站建设 2026/10/18 12:04:22

AI编程API中断应对指南:安全、可观测性与弹性架构

1. 事件起点&#xff1a;一个被突然切断的API连接我第一次意识到不对劲&#xff0c;是在某个周三下午三点十七分。当时正在调试一个跨平台文档协同工具的代码补全模块&#xff0c;后端调用的是 Claude Code 的 API 接口。前一秒还在稳定返回带注释的 Python 函数片段&#xff0…

作者头像 李华
网站建设 2026/10/19 16:56:04

siamfC项目拆解:孪生网络与特征比较实战指南

很多人第一次看到“siamfC”这个项目代号时&#xff0c;第一反应都是愣一下。小写开头、中间夹个大写F、末尾再来个大写C&#xff0c;既不像是标准的驼峰命名&#xff0c;也看不出某个单词的直接缩写&#xff0c;乍一看还以为是随手敲键盘敲出来的乱码。但如果你手上的确拿到了…

作者头像 李华
网站建设 2026/10/18 4:26:54

Git源码代管理实战:从版本控制到协作操作系统

1. 这不是“又一个Git教程”&#xff0c;而是你真正能用起来的源码管理实战手册 我带过不少刚接触代码协作的新手&#xff0c;也帮某高校实验室重构过三个跨年级团队的项目交付流程。最常听到的一句话是&#xff1a;“Git命令背了一堆&#xff0c;一到实际改需求、合代码、回滚…

作者头像 李华
网站建设 2026/10/18 12:12:26

智能助手开发:Task、Session、Action 分离管理实战

1. 从一个真实翻车现场说起去年我参与过一个内部工具的开发&#xff0c;核心功能是让一个智能助手帮用户处理多步骤的文档整理任务。上线第一周就收到一条反馈&#xff1a;用户上午让助手整理了一份会议纪要&#xff0c;中途去开会了&#xff0c;下午回来接着聊&#xff0c;助手…

作者头像 李华
网站建设 2026/10/19 9:33:23

SiamFC复现指南:全卷积孪生网络如何将目标跟踪变成一次卷积

说句实话&#xff0c;目标跟踪方向这几年迭代太快&#xff0c;新模型一个接一个&#xff0c;但我始终觉得有一份基础工作值得反复读、反复复现&#xff0c;那就是 SiamFC&#xff0c;全称 Siamese Fully Convolutional Network&#xff0c;也就是全卷积孪生网络。它的项目标题“…

作者头像 李华