告别“我觉得”!用 JMH 搞懂你的 Java 代码性能

文章首发公众号『风象南』

项目开发中,经常会遇到这样的情况:辛辛苦苦写了一段代码,觉得自己优化得不错,性能肯定提升了。 然而, "我觉得"往往不靠谱。 没有经过严谨的测试,一切都是空谈。 这时候,你就需要 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 快速上手:一个简单的例子

说了这么多,不如来个实际例子。 假设我们要测试两种字符串拼接方式的性能

    1. 使用 + 运算符
    1. 使用 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,可以让你对自己的代码性能更有信心,避免"我觉得"式的盲目优化。

相关推荐
HZ_YZ15 小时前
清理服务器硬盘脚本
后端
布朗克16815 小时前
Go 入门到精通-16-字符串深入
开发语言·后端·golang·字符串
北冥you鱼16 小时前
Go flag 包详解:从命令行解析到实战应用
开发语言·后端·golang
蓝田~16 小时前
大模型本地部署与远程调用 — 从 API 到 Agent
java·人工智能·claude·claude code
码农进化录16 小时前
Java 程序员的 AI 进化论 | Spring Boot 接入 OpenAI 的六个坑,全帮你踩了
java·spring boot·openai
她说彩礼65万16 小时前
Asp.net core 模型绑定
后端·asp.net
用户342323237631716 小时前
BLE 传感器节点实战
后端
疯狂成瘾者16 小时前
Java 常见集合方法
java·windows·python
从此以后自律16 小时前
ConcurrentHashMap 超详细详解
java
fliter16 小时前
Go 1.26 新增 `bytes.Buffer.Peek`:只看数据,不移动读取位置
后端