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

相关推荐
天草二十六_简村人27 分钟前
kong搭建一套微信小程序的公司研发环境
java·后端·微信小程序·小程序·kong
菜鸟一皓35 分钟前
告别XML模板的繁琐!Word文档导出,easy!
java·word
乐闻x43 分钟前
性能优化:javascript 如何检测并处理页面卡顿
前端·javascript·性能优化
2401_884810742 小时前
maven笔记
java·笔记·maven
霸王龙的小胳膊2 小时前
SpringMVC-登录校验
java·mvc
字节源流2 小时前
【SpringMVC】常用注解:@PathVariable
java·开发语言·servlet
小安同学iter2 小时前
SpringMVC(五)拦截器
java·开发语言·spring boot·spring·java-ee
鱼樱前端2 小时前
前端程序员集体破防!AI工具same.dev像素级抄袭你的代码,你还能高傲多久?
前端·javascript·后端
栀栀栀栀栀栀2 小时前
JVM 2015/3/15
java·开发语言·jvm
羊思茗5202 小时前
Spring Boot中@Valid 与 @Validated 注解的详解
java·spring boot·后端