性能工程:容量、基准、剖析与 JVM 诊断
优化之前,先把“慢”说具体:哪条操作,在多大负载下,等待了多久?然后保留相同条件,一次改一个因素。下面把容量测试和 JVM 诊断连起来,避免把偶然波动当成优化效果。
优化目标应是可度量的吞吐、延迟分位数、资源与成本,而不是抽象的“更快”。
1. 本文覆盖范围
- SLO 与容量模型
- 正确基准测试
- CPU/内存/锁/IO 剖析
- JFR、jcmd、GC 日志与压测闭环
2. 核心知识详解
1. 预算与排队
吞吐、并发与平均响应时间可用 Little 定律在稳定系统中关联。端到端延迟预算分配给网关、排队、服务、数据库和网络。
- 使用 p50/p95/p99 和错误率,不只看平均值。
- 容量测试逐步升压,识别饱和点和拐点。
- 稳态、突发、故障和恢复分别验证。
这里容易混淆的是: Little 定律要求稳定长期平均;系统积压持续增长时直接套用会误导。
2. 基准测试方法
JVM 有 JIT、逃逸分析、GC 和预热,微基准使用 JMH 避免死代码消除、常量折叠和错误计时。
- 端到端压测与微基准回答不同问题。
- 固定数据、环境、版本和负载生成器容量。
- 报告置信区间、样本和火焰图证据。
这里容易混淆的是: 一次 System.nanoTime 循环不是可靠 JVM 微基准。
3. 证据驱动剖析
CPU profile 找热点,allocation profile 找分配,heap dump 找保留路径,线程转储找阻塞,GC 日志解释暂停与堆行为。
- 先复现并建立基线,再改一个变量。
- 区分 CPU 饱和、锁等待、IO 等待和下游排队。
- 生产优先低开销 JFR,敏感数据采集有权限和留存控制。
这里容易混淆的是: 对象数量多不等于泄漏;从 GC Roots 的意外保留路径才是关键证据。
4. 优化与验证
优化顺序通常是删除不必要工作、改算法/查询、批量与缓存、减少分配/锁,最后才微调 JVM 参数。
- 每次优化验证正确性和尾延迟。
- GC 选择依据堆、暂停目标和吞吐。
- 变更设回滚阈值和线上观测。
这里容易混淆的是: 增加堆可能减少 GC 频率,也可能增加内存成本和故障转储时间;需要测量。
3. 工程链路
4. 操作练习:给真实慢请求收集一份 JFR
前置条件是本机已有运行中的 HotSpot Java 服务,你有权附加到该进程,磁盘有足够空间。以下命令使用 JDK 自带工具;把 12345 换成 jcmd -l 列出的目标 PID,不要把它当成固定端口。
jcmd -l
jcmd 12345 JFR.start name=slow-request settings=profile duration=60s filename=slow-request.jfr
jcmd 12345 JFR.check
jfr summary slow-request.jfr
在这 60 秒内用同样的输入重复那条慢操作。录制完成后再运行 jfr summary,并用 JDK Mission Control 打开文件。先看线程是在运行还是等待,再检查 CPU 样本、分配、锁和 GC 事件。采样事件是统计证据,某个栈没有出现不代表它从未执行;录制策略和事件阈值也会影响可见性。
举例:如果请求慢时线程主要等数据库连接,先查池等待和慢 SQL;盲目改 Java 循环不会解决等待。若 CPU 样本集中在解析同一份大配置,才有理由研究重复解析。记录优化前后的负载、成功率、p95 和资源占用,不要只保存“感觉变快”的截图。
settings=profile 有额外开销,应先在测试环境估计影响。采集文件可能含业务路径、线程名等信息,分享前检查内容。
5. 实践与验证
- 用 JMH 比较两种集合查找,解释预热和 Blackhole。
- 对一个接口采集 JFR 并区分 CPU、锁与分配热点。
- 写出峰值、实例数、连接池和数据库容量表。
6. 掌握检查
- 能使用分位数和饱和点。
- 能写可信 JMH。
- 能读基础 JFR/线程/堆证据。
- 能验证优化收益与回归。