有效性能基线必须同时使用JMH做代码级微基准测试和Async Profiler配合wrk等压测工具采集真实负载指标;仅靠wrk/JMeter的宏观指标无法定位延迟毛刺根源,如Full GC停顿或ConcurrentHashMap扩容锁竞争。

直接给结论:用 JMH 测代码级微基准,用 Async Profiler + 压测工具(如 wrk)跑真实负载并采集运行时指标,两者缺一不可。只跑一次压测、只看平均响应时间、不记录 GC 日志,都不算建立了有效基线。
为什么不能只靠 wrk 或 JMeter 跑一遍就叫基线
压测工具输出的 avg latency、req/s 是宏观结果,但无法告诉你延迟毛刺来自哪里。比如 TP99 突然飙高,可能是某次 Full GC 停顿了 800ms,也可能是 ConcurrentHashMap 在扩容时锁住了读线程 —— 这些细节压测工具本身不暴露。
常见错误现象包括:
- 同一台机器、相同命令反复跑
wrk -t12 -c300 -d30s,TP99 波动从 120ms 到 450ms,却归因为“网络抖动” - 压测期间
jstat -gc显示G1 Young GC次数翻倍,但没保存日志,事后无法回溯是否因堆内存分配速率突增导致 - 未启用
-XX:+PrintGCDetails -Xlog:gc*,GC 行为完全黑盒,调优变成凭感觉
JMH 不是“写个 main 方法测三次取平均”
JMH 的核心价值在于规避 JVM 预热、死码消除、常量折叠等干扰。你写的 @Benchmark 方法如果返回值没被消费,JIT 可能直接优化掉整段逻辑 —— 这就是为什么必须用 Blackhole.consume() 或显式赋值到 @State 对象里。
立即学习“Java免费学习笔记(深入)”;
实操建议:
将后端 Java 接口文档转换为前端 TypeScript 类型定义(interface / type)。输入:飞书文档、Spring Boot 接口文档、Swagger/OpenAPI、手写 API 文档等。输出:包含请求参数与响应结果的 .md 文件,含完整 JSDoc 注释。触发关键词:Java 转 TS、接口转换、生成前端类型、接口文档转 TypeScript、DTO/VO/BO 转 interface、后端文档转前端类型、生成接口类型、类型定义生成。
- 每个
@Benchmark方法必须标注@Fork(jvmArgsAppend = {"-Xmx2g", "-XX:+UseG1GC"}),确保 JVM 参数可控,避免继承宿主环境的默认值 - 禁用
@Warmup(iterations = 0)—— 至少iterations = 5,预热不足会导致首轮测量严重偏低 - 不要在
@Setup里创建大对象池(如 10 万个String),这会污染预热阶段的 GC 行为,应改用@Param控制规模 - 生成报告时加
-rf json,后续可脚本化比对不同版本的scoreError是否在误差范围内
Async Profiler 必须和压测节奏对齐
async-profiler 本身不采样“慢请求”,它采样的是 CPU 时间片或分配事件。所以必须在压测进行中启动 profiling,否则拿到的是空闲期的火焰图,毫无参考价值。
典型误操作:
- 压测结束才执行
./profiler.sh -e alloc -d 30 -f alloc.html <pid></pid>,此时对象已回收,分配热点丢失 - 用
-e cpu但没加-j(Java-only mode),结果火焰图里混入大量libc和内核栈,掩盖 Java 方法真实耗时 - 采样间隔设成
-i 1000000(1ms),在高并发下产生巨量数据,反而拖慢应用;生产环境建议用默认 10ms 或 20ms - 没配合
-o collapsed输出折叠格式,导致无法用flamegraph.pl生成可交互火焰图
推荐组合命令:wrk -t12 -c300 -d60s http://localhost:8080/api/order && ./profiler.sh -e cpu -j -d 60 -f cpu.svg <pid></pid>,确保压测与 profiling 时长一致。
基线数据必须带上下文存档,否则半年后没人看得懂
一次有效的基线不是几个数字,而是一组带元信息的快照。最容易被忽略的是 JVM 启动参数和系统约束的耦合关系 —— 比如容器里设了 memory: 4g,但没配 -XX:+UseContainerSupport,JVM 仍按宿主机内存算堆大小,导致 MaxHeapSize 远超限制,频繁 OOM。
每次存档至少包含:
-
java -version输出(确认是OpenJDK 17.0.8+7还是21.0.6+7) - 完整 JVM 启动参数(尤其注意
-XX:MaxRAMPercentage和-XX:+UseG1GC是否启用) -
cat /sys/fs/cgroup/memory.max(容器环境)或free -h(物理机),说明内存边界 -
wrk命令原文 + 实际执行时间戳,而非“跑了 30 秒”这种模糊描述 -
async-profiler生成的.svg和.jfr文件,命名含日期、场景、JDK 版本(如cpu_20261015_order_create_jdk17.svg)
复杂点在于:基线不是静态快照,而是随 JDK 小版本升级、G1 参数微调、甚至 Linux 内核补丁更新而漂移的参考系。今天建的基线,三个月后可能因 OpenJDK 17.0.9 修复了一个 JIT 编译器 bug 而整体提升 8%,这时候比对必须明确标注 JDK build 号,而不是只写 “JDK 17”。


















