JMH是Java最可靠的微基准测试工具,需规范配置预热、fork、JVM参数及避免副作用;功能级压测宜用线程池+Future自主计时;JMeter易受外部因素干扰,应结合jstack/jstat和代码打点定位瓶颈。
怎么用 JMH 写出靠谱的并发基准测试
直接手写 Thread 启动一百个任务再算耗时,结果基本不可信——JVM 预热没做、GC 干扰没控、测量方式不原子,测出来的数字连趋势都反映不了。JMH 是目前 Java 生态最可靠的微基准测试工具,它强制你面对预热、fork、统计方式这些关键控制点。
关键实操建议:
- 必须用
@Fork(jvmArgsAppend = {"-Xmx2g", "-XX:+UseG1GC"})显式指定 JVM 参数,否则默认 fork 的 JVM 可能用 CMS 或小堆,干扰吞吐量对比 -
@State(Scope.Benchmark)下的共享对象(比如ConcurrentHashMap)要小心:多个线程共用同一实例,但 JMH 会为每个 fork 单独初始化一次,这点比手写测试更可控 - 避免在
@Benchmark方法里做 I/O、日志、随机数生成等副作用操作;如果真要测带锁逻辑,用@Group("lock")+@GroupThreads(4)模拟竞争更真实
@Fork(3)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
public class ConcurrentHashMapPutBenchmark {
private ConcurrentHashMap map;
@Setup
public void setup() {
map = new ConcurrentHashMapzuojiankuohaophpcnyoujiankuohaophpcn();
}
@Benchmark
public void put(@Param({"1000", "10000"}) int size) {
for (int i = 0; i zuojiankuohaophpcn size; i++) {
map.put(i, "val" + i);
}
}
}
ThreadPoolExecutor + CountDownLatch 做功能级压测时的陷阱
当你要验证“接口在 500 QPS 下是否稳定”,而不是测单个方法的纳秒级性能时,JMH 就不合适了。这时候得自己搭轻量压测主干,但多数人栽在资源泄漏和计时偏差上。
立即学习“Java免费学习笔记(深入)”;
常见错误现象:
- 用
System.nanoTime()包裹整个executor.invokeAll(tasks)—— 实际测的是任务提交耗时,不是执行耗时 -
CountDownLatch在异常路径下没被countDown(),导致主线程永远阻塞 - 线程池
corePoolSize设太小(比如 2),大量任务排队,ThreadPoolExecutor.getCompletedTaskCount()返回值远低于预期,并发度根本没打上去
正确做法是每个任务自己记录执行时间,并汇总统计: