项目开发中,经常会遇到这样的情况:辛辛苦苦写了一段代码,觉得自己优化得不错,性能肯定提升了。 然而, “我觉得”往往不靠谱。 没有经过严谨的测试,一切都是空谈。 这时候,你就需要 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,可以让你对自己的代码性能更有信心,避免“我觉得”式的盲目优化。