文章首发公众号『风象南』
项目开发中,经常会遇到这样的情况:辛辛苦苦写了一段代码,觉得自己优化得不错,性能肯定提升了。 然而, "我觉得"往往不靠谱。 没有经过严谨的测试,一切都是空谈。 这时候,你就需要 JMH (Java Microbenchmark Harness) 这个利器了。
JMH 是什么?它能做什么?
简单来说,JMH 是 Oracle 官方提供的、专门用于 Java 代码性能测试的框架。 它可以帮你
- 精确测量代码片段的性能: 避免手写测试代码带来的误差,例如 JVM 预热、JIT 优化等。
- 发现潜在的性能瓶颈: 通过各种统计指标,例如平均执行时间、吞吐量等,让你对代码的性能一目了然。
- 对比不同实现的性能差异: 轻松对比多种方案,选择最优解。
- 避免常见的性能测试陷阱: JMH 会自动处理很多细节,让你专注于测试逻辑本身。
为什么要用 JMH?"土法炼钢"不行吗?
可能有些同学会说:"我自己写个 main 方法,跑个几百万次,也能测出性能啊!" 听起来似乎可行,但其实有很多坑
- JVM 预热 (Warmup): JVM 在程序刚启动时,性能往往比较差,需要经过一段时间的预热才能达到最佳状态。 手动测试很难准确控制预热时间。
- JIT 优化: JVM 会对热点代码进行即时编译 (JIT),将其编译成本地代码,从而提高性能。 手动测试很难模拟 JIT 优化后的真实情况。
- 死代码消除 (Dead Code Elimination): 如果你的测试代码中有些部分没有被实际使用,JVM 可能会将其优化掉,导致测试结果不准确。
- 伪共享 (False Sharing): 在多线程环境下,不同线程操作相邻的内存区域时,可能会导致缓存失效,从而影响性能。
而 JMH 会自动处理这些问题,保证测试结果的准确性和可靠性。
JMH 快速上手:一个简单的例子
说了这么多,不如来个实际例子。 假设我们要测试两种字符串拼接方式的性能
-
- 使用 + 运算符
-
- 使用 StringBuilder
首先,我们需要添加 JMH 的依赖。 如果你使用 Maven,可以在 pom.xml 文件中添加以下内容
xml
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
</dependency>
然后,创建一个测试代码 ,内容如下
java
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@State(Scope.Thread)
public class StringConcatBenchmark {
@Param({"10", "100", "1000"})
public int length;
private String str;
@Setup(Level.Trial)
public void setup() {
str = "a";
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void testStringConcat(Blackhole blackhole) {
String result = "";
for (int i = 0; i < length; i++) {
result += str;
}
blackhole.consume(result);
}
@Benchmark
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public void testStringBuilder(Blackhole blackhole) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < length; i++) {
sb.append(str);
}
blackhole.consume(sb.toString());
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(StringConcatBenchmark.class.getSimpleName())
.forks(1)
.warmupIterations(5)
.measurementIterations(5)
.build();
new Runner(opt).run();
}
}
- @State(Scope.Thread): 表示每个线程都拥有一个 StringConcatBenchmark 实例。
- @Param({"10", "100", "1000"}): 表示 length 字段有三个不同的取值,JMH 会分别对这三个值进行测试。
- @Setup(Level.Trial): 表示在每次测试之前都会执行 setup 方法,用于初始化 str 字段。
- @Benchmark: 表示这是一个需要进行性能测试的方法。
- @BenchmarkMode(Mode.AverageTime): 表示以平均执行时间作为测试指标。
- @OutputTimeUnit(TimeUnit.NANOSECONDS): 表示测试结果以纳秒为单位输出。
- Blackhole.consume(result): 用于防止 JIT 优化将 result 变量优化掉,保证测试的准确性。
最后,运行 main 方法,就可以看到 JMH 的测试结果了。
JMH 进阶用法
除了上面介绍的基本用法,JMH 还有很多高级功能,例如
- 使用 @CompilerControl 控制 JIT 优化
- 使用 @BenchmarkMode 选择不同的测试模式 (例如吞吐量模式、单次执行时间模式)
- 自定义 State 对象,实现更复杂的测试场景
- 使用 JMH 的 API 手动控制测试过程
总结
JMH 是一个非常强大的 Java 性能测试框架。 掌握 JMH,可以让你对自己的代码性能更有信心,避免"我觉得"式的盲目优化。