资讯动态

Java性能优化01:性能优化不是调参数,先建立目标、指标与优化闭环

发布时间:2026/10/15 19:22:28 来源:尧图企业网站定制
Java性能优化01性能优化不是调参数先建立目标、指标与优化闭环Java 性能优化系列第 1 篇。本文先回答一个最基本的问题怎样判断一次优化真的有效CPU、内存、GC、多线程、JDK 21 虚拟线程和网络问题会在后续文章中分别实践。接口变慢后很多人的第一反应是加线程、扩大堆内存、修改 GC 参数。这些办法有时有效但也可能完全改错方向。性能优化更像看病先确认症状再用检查结果找原因最后用复查判断治疗是否有效。不能只因为改了参数、平均耗时下降就宣布优化成功。错误率可能升高少数请求可能变得更慢代码甚至可能少做了业务工作。开始优化前先回答四个问题测的是哪个业务场景测试时有多少流量、多少数据哪个数字达到多少才算成功优化后业务结果是否仍然正确一、把“快一点”改成明确目标“把报价接口优化一下”不是一个可以验收的目标。可以改成使用固定测试数据持续压测 30 分钟。99% 的请求在 200 ms 内完成也就是 p99 小于 200 ms。错误率小于 0.1%单实例 CPU 不超过 70%。返回结果必须与优化前一致。这句话说明了五件事要说明的内容例子测什么/api/quote报价接口怎么测固定数据持续压测 30 分钟多快算合格99% 的请求在 200 ms 内完成p99 小于 200 ms能否出错错误率小于 0.1%有什么限制CPU 不超过 70%业务结果不能改变这些目标不能凭感觉填写。它们应该来自用户能接受的等待时间、上游超时时间和机器成本。二、性能至少要看四类数字不能只看一个“平均响应时间”。一次测试至少要同时记录下面四类指标指标说人话常见单位或例子吞吐量每秒真正完成了多少工作req/s、条/秒延迟用户等了多久p50、p95、p99错误率有多少请求失败或超时0.1%、1%资源为这些结果花了多少机器资源CPU、内存、GC垃圾回收、线程、连接数吞吐量每秒完成多少请求一个接口每秒返回 1,000 次可以写成 1,000 req/s。但要注意快速返回错误也会产生很高的 req/s所以吞吐量必须和错误率一起看。延迟不要只看平均值把 100 个请求按耗时从小到大排列p50 可以理解为中间水平约有一半请求比它快。p95 表示约 95% 的请求不超过这个耗时。p99 关注最慢的那一小部分请求也就是常说的“长尾”。例如99 个请求都用了 10 ms只有 1 个用了 1 秒平均值仍然只有约 19.9 ms。平均值看起来很好但那个等待 1 秒的用户并不会感受到“平均”。错误率不能用失败换速度优化后延迟下降但错误率从 0.1% 升到 2%通常不能算成功。除了 HTTP 500还应统计超时、限流和业务校验失败。资源解释为什么慢CPU 高可能是计算太多CPU 不高但延迟很高线程可能在等数据库或下游接口p99 周期性升高可能与 GC 停顿有关。GC 是 JVM 自动清理不再使用对象的过程清理期间可能短暂停顿应用。这里的“可能”很重要。监控只能帮助提出猜测不能直接证明原因。真正下结论前还要查看线程、GC 日志、JFRJDK 自带的性能记录工具或火焰图显示程序时间主要花在哪些方法上的图并做对照实验。补充SLI、SLO 和 SLA理解了指标之后再看三个常见缩写就简单多了SLI 是实际成绩例如本月 99.95% 的请求成功。SLO 是目标成绩例如本月成功率要达到 99.9%。SLA 是对外承诺没有达到时可能需要赔偿。可以把它们记成SLI 是成绩单上的分数SLO 是目标线SLA 是写进合同的承诺。k6 压测通过只能说明这次测试满足条件不能证明整个月的生产服务都满足 SLO。三、性能优化的正确顺序一次完整优化可以概括成六步定目标、测基线、找证据、改一处、重新测、再上线。这里有三条硬规则业务结果不能变错不能靠少查数据、少做计算换取低延迟。前后测试条件要相同改代码的同时换机器就无法判断收益来自哪里。测试要重复执行第一次可能受 Java 预热、连接建立或缓存状态影响。四、贯穿本系列的实验项目本系列使用开源项目java-performance-lab。它提供三个接口和一个可控制延迟、失败的模拟下游服务。这里的“下游服务”就是当前接口还要调用的另一个服务。接口做什么后续用来测试什么/api/search查询数据库并返回 JSON慢 SQL、连接池、分页、序列化/api/quote读取缓存并调用下游接口超时、缓存、线程阻塞、虚拟线程、网络/api/report做计算并批量处理数据CPU、对象分配、锁竞争、GC后续文章都使用这三个接口。这样每次优化都能和前面的数据比较不需要每篇重新编一个案例。五、跑一次真实基线“基线”就是修改代码之前的成绩。没有基线优化后就没有可靠的比较对象。测试环境本次数据采集于 2026-10-14应用代码版本为34f3562。项目配置操作系统Windows 11压测工具和服务在同一台机器CPUIntel Core i5-1350014 核 20 线程内存32 GB应用容器没有单独限制 CPU 和内存JavaEclipse Temurin 21.0.12.1 LTS应用Spring Boot 3.4、H2、无 Redis 缓存、普通平台线程传统 Java 线程压测工具k6 v2.1.0压测端和服务端在同一台电脑而且容器没有固定资源因此这组数据只用于演示测试过程不代表项目的最大容量。怎么测本次使用 k6 压测。k6 是一个开源压测工具可以模拟多个用户反复访问接口并统计吞吐量、响应时间和错误率。基线脚本是load-tests/baseline.js。测试方式如下使用 5 个虚拟用户也就是让 k6 模拟 5 个并发用户持续 20 秒。每轮同时请求三个接口然后等待 200 ms。三个接口都必须返回 HTTP 200也就是请求成功。整体错误率必须低于 1%p95 必须低于 1 秒。下面是一条完整命令在 PowerShell、cmd 和 Bash 中都不需要处理换行符k6 run --summary-trend-stats avg,min,med,p(90),p(95),p(99),max --summary-export blogs/assets/01/baseline-run-1.json -e BASE_URLhttp://localhost:8080 load-tests/baseline.js仓库中的标准 Compose 环境使用 8080 端口。本文采集数据时开发容器映射到了 18080两者只是端口映射不同。连续测三次代码和测试条件都不变连续执行三次轮次请求数吞吐p50p95p99错误率第 1 次1,44071.69 req/s4.58 ms11.60 ms30.75 ms0%第 2 次1,47073.07 req/s3.45 ms5.55 ms7.24 ms0%第 3 次1,47073.23 req/s3.11 ms4.98 ms6.10 ms0%数据说明数据来自仓库中的三份baseline-run-*.json图片由render-baseline-chart.ps1生成。三次测试的错误率都是 0%。最明显的现象是第一次的 p99 为 30.75 ms第三次只有 6.10 ms相差约 5 倍但三次吞吐量很接近。这只能证明一件事这种短时间测试的长尾不够稳定不能跑一次就挑一个好看的结果。现在还不能断定是 JIT 预热造成的。JIT 会在程序运行期间把经常执行的 Java 代码进一步编译成本机更容易执行的机器码因此程序刚启动时和运行一段时间后速度可能不同。连接建立、缓存和电脑上的其他任务也可能影响结果。要确定原因需要延长测试时间并结合 JFRJDK 自带的性能记录工具等证据这会在后续文章中完成。第 2 次测试中三个接口的结果如下接口p50p95p99/api/search3.01 ms4.15 ms6.78 ms/api/quote4.53 ms5.88 ms7.14 ms/api/report3.07 ms4.18 ms6.41 ms报价接口略慢但差距很小还不能说明下游 HTTP 就是瓶颈。下一步应该增加下游延迟做对照实验而不是马上修改线程池或 HTTP 参数。六、补充知识用利特尔法则理解“系统里有多少请求”Little’s Law 中文常叫“利特尔法则”。名字听起来有些陌生它表达的意思其实很直观请求来得越快或者每个请求停留得越久系统里同时存在的请求通常就越多。公式如下系统内平均请求数 每秒处理的请求数 × 每个请求的平均停留时间秒 L λ × WL是系统内的平均请求数包括正在处理和排队等待的请求。λ读作“兰姆达”表示平均吞吐量例如每秒处理 500 个请求。W表示一个请求从进入到离开的平均时间单位必须换算成秒。先看一个容易理解的例子。一个接口平均每秒处理 500 个请求每个请求平均停留 200 ms也就是 0.2 秒500 × 0.2 100 个请求这表示在稳定运行期间系统中平均大约有 100 个请求正在处理或排队。它不代表线程池必须设置为 100因为请求还可能受 CPU、数据库连接池、下游接口和线程模型影响。再看本文的实测数据。第 2 次测试的吞吐量为 73.07 req/s平均响应时间为 3.67 ms也就是 0.00367 秒73.07 × 0.00367 ≈ 0.27 个请求“0.27 个请求”是整个测试期间算出来的平均值并不是说某个时刻真的存在四分之一个请求。这个数字小于 1是因为接口响应很快而且 5 个虚拟用户每请求一轮还会等待 200 ms大部分时间里应用内没有请求正在执行。使用这个公式要满足三个条件统计时间足够长、请求没有持续越积越多、吞吐量和响应时间使用同一个系统范围。它适合检查数据是否大致合理但不能直接用来决定线程池大小也不能把 p99 当成公式中的平均响应时间。七、怎样提出一个可以验证的猜测不要只说“虚拟线程更快”而要把猜测写完整如果给下游增加 100 ms 延迟普通平台线程传统 Java 线程会大量等待。启用 JDK 21 虚拟线程一种可以大量创建的轻量线程后线程资源占用应该下降但数据库连接数和下游处理能力没有变化所以系统吞吐不会无限增加。这句话说明了测试场景、准备修改的内容和预期结果。如果结果没有出现这个猜测就可能是错的。每次实验至少记录下面这些内容- 日期和代码版本 - JDK、机器或容器配置 - 测试数据和负载 - 优化目标 - 业务正确性检查 - 观察到的现象 - 本次只修改什么 - 优化前后数据 - 最终决定保留、回滚还是继续实验更完整的模板在docs/results/README.md。八、最容易踩的六个坑只看平均值平均值会隐藏少量特别慢的请求至少还要看 p95、p99 和错误率。只测一次Java 预热、缓存和连接状态都会影响短测试。测试时间太短20 秒看不到内存逐渐增长、缓存过期和周期性 GC。压测工具和服务抢同一台机器正式容量测试应把压测端分开并检查压测机自己有没有到上限。优化前后做的工作不一样少返回字段、跳过校验会让数字变好但这不是正常优化。一次修改很多地方同时改 GC、线程池和缓存即使变快也不知道究竟是哪一项有效。小结第一次做性能优化可以先记住下面这套简单方法写清业务场景和合格标准。同时记录吞吐、延迟、错误率和资源。修改前先测基线而且要重复测试。根据证据猜原因一次只改一个主要因素。用相同条件重新测试并检查业务结果。小流量上线指标异常时能够回滚。下一篇《Java性能优化线上 Java 性能问题排查总纲覆盖 CPU、线程、内存与 GC》会回答CPU 高、线程卡住、内存上涨或频繁 GC 时第一步应该看什么。文章会使用jcmd、JFR/JDK Mission Control、async-profiler 和 Arthas并配上真实工具图。系列导航上一篇无 | 系列目录 | 下一篇Java性能优化线上 Java 性能问题排查总纲覆盖 CPU、线程、内存与 GC

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑