告别“我觉得”!用 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,可以让你对自己的代码性能更有信心,避免"我觉得"式的盲目优化。

相关推荐
星霜旅人6 分钟前
Java并发编程
java
天上掉下来个程小白44 分钟前
缓存套餐-01.Spring Cache入门案例
java·redis·spring·缓存·springboot·springcache
深色風信子1 小时前
Eclipse 插件开发 6 右键菜单
java·ide·eclipse·右键菜单
网安INF1 小时前
Apache Shiro 1.2.4 反序列化漏洞(CVE-2016-4437)
java·网络安全·apache
RedJACK~1 小时前
Go语言Stdio传输MCP Server示例【Cline、Roo Code】
开发语言·后端·golang
it-搬运工1 小时前
远程调用负载均衡LoadBalancer
java·微服务·负载均衡
努力努力再努力wz1 小时前
【Linux实践系列】:进程间通信:万字详解共享内存实现通信
java·linux·c语言·开发语言·c++
GISer_Jing2 小时前
前端工程化和性能优化问题详解
前端·性能优化
-曾牛2 小时前
Azure OpenAI 聊天功能全解析:Java 开发者指南
java·开发语言·人工智能·spring·flask·azure·大模型应用
bing_1582 小时前
Spring Boot 中如何启用 MongoDB 事务
spring boot·后端·mongodb