当前位置: 首页 > news >正文

JMH 基准测试实战:Java 性能对比的正确打开方式!

📖 摘要

在Java开发中,我们经常需要比较不同实现方式的性能差异。但如何科学、准确地进行性能测试呢?本文将带你深入理解JMH(Java Microbenchmark Harness)工具,通过实战演示如何正确编写和运行基准测试,避免常见的性能测试陷阱,让你的性能对比结果真实可靠!

📚 目录

  1. 为什么需要JMH?
  2. JMH基础概念
  3. 环境准备
  4. 第一个JMH基准测试
  5. 常用注解详解
  6. 避免性能测试陷阱
  7. 实战案例:字符串拼接性能对比
  8. 高级特性
  9. 总结

🤔 为什么需要JMH?

在日常开发中,我们经常会遇到这样的场景:

long start = System.currentTimeMillis();
// 测试代码
long end = System.currentTimeMillis();
System.out.println("耗时:" + (end - start) + "ms");

这种简单粗暴的性能测试方式存在很多问题:

  1. JVM预热问题:JVM需要时间"热身",初始执行的代码通常比后续执行慢
  2. 编译器优化:JIT编译器可能会优化掉"无用"代码
  3. 统计误差:单次测试结果不具有代表性
  4. 环境干扰:GC、CPU调度等都会影响结果

JMH是OpenJDK团队开发的专门用于Java微基准测试的工具,它能解决上述所有问题,提供科学、准确的性能测试结果。

📖 JMH基础概念

JMH中有几个核心概念需要理解:

  1. @Benchmark:标记要测试的方法
  2. Mode:测试模式,如吞吐量(Throughput)、平均时间(AverageTime)等
  3. Warmup:预热阶段,让JVM达到稳定状态
  4. Measurement:实际测量阶段
  5. Fork:在独立的JVM进程中运行测试,避免相互干扰
  6. State:测试状态,可用于共享数据

🛠️ 环境准备

1. 添加JMH依赖

使用Maven项目,添加以下依赖:


    org.openjdk.jmh
    jmh-core
    1.36


    org.openjdk.jmh
    jmh-generator-annprocess
    1.36
    provided

2. IDE插件(可选)

IntelliJ IDEA有JMH插件,可以方便地运行单个基准测试方法。

🚀 第一个JMH基准测试

让我们从一个最简单的例子开始:

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.AverageTime) // 测试平均执行时间
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 输出结果的时间单位
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) // 预热3轮,每轮1秒
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测试5轮,每轮1秒
@Fork(1) // 使用1个进程
@State(Scope.Thread) // 每个测试线程一个实例
public class FirstBenchmark {

    @Benchmark
    public void testMethod() {
        // 这里是被测试的方法
        int a = 1;
        int b = 2;
        int sum = a + b;
        // 防止被JIT优化掉
        if (sum != 3) {
            throw new AssertionError();
        }
    }

    public static void main(String[] args) throws Exception {
        org.openjdk.jmh.Main.main(args);
    }
}

运行这个基准测试,你会看到类似如下的输出:

Benchmark                Mode  Cnt  Score   Error  Units
FirstBenchmark.testMethod  avgt    5  0.321 ± 0.012  ns/op

这表示我们的测试方法平均每次执行耗时0.321纳秒。

🏷️ 常用注解详解

JMH提供了丰富的注解来配置基准测试,下面介绍最常用的几个:

1. @BenchmarkMode

定义测试模式,可选值:

  • Throughput:吞吐量,单位时间内的操作数
  • AverageTime:平均时间,每次操作耗时
  • SampleTime:采样时间
  • SingleShotTime:单次执行时间
  • All:所有模式
@BenchmarkMode({Mode.Throughput, Mode.AverageTime})

2. @Warmup

配置预热参数:

  • iterations:预热轮数
  • time:每轮时间
  • timeUnit:时间单位
@Warmup(iterations = 3, time = 100, timeUnit = TimeUnit.MILLISECONDS)

3. @Measurement

配置实际测量参数:

  • iterations:测量轮数
  • time:每轮时间
  • timeUnit:时间单位
@Measurement(iterations = 5, time = 100, timeUnit = TimeUnit.MILLISECONDS)

4. @Fork

指定fork的进程数,避免测试间相互干扰:

@Fork(2) // 使用2个独立进程

5. @State

定义状态对象,用于在不同测试方法间共享数据:

@State(Scope.Thread) // 每个线程一个实例
public class MyState {
    public int a = 1;
    public int b = 2;
}

@Benchmark
public void testMethod(MyState state) {
    int sum = state.a + state.b;
}

⚠️ 避免性能测试陷阱

1. 死代码消除(Dead Code Elimination)

JIT编译器会优化掉没有实际效果的代码。例如:

@Benchmark
public void wrongTest() {
    Math.log(1.0); // 结果没有被使用,会被优化掉
}

正确做法是返回计算结果或使用Blackhole

@Benchmark
public double correctTest() {
    return Math.log(1.0);
}

// 或者
@Benchmark
public void correctTest2(Blackhole blackhole) {
    blackhole.consume(Math.log(1.0));
}

2. 常量折叠(Constant Folding)

JIT会预先计算常量表达式:

@Benchmark
public int wrongTest() {
    return 1 + 2; // 会被优化为return 3
}

使用@State注入变量:

@State(Scope.Thread)
public class MyState {
    public int a = 1;
    public int b = 2;
}

@Benchmark
public int correctTest(MyState state) {
    return state.a + state.b;
}

3. 循环展开(Loop Unrolling)

JMH会自动处理循环问题,不要手动写循环:

// 错误做法
@Benchmark
public void wrongTest() {
    for (int i = 0; i < 10000; i++) {
        // 测试代码
    }
}

// 正确做法
@Benchmark
public void correctTest() {
    // 单次操作的代码
}

JMH会自动控制迭代次数来获得准确的测量结果。

🎯 实战案例:字符串拼接性能对比

让我们通过一个实际案例来比较不同字符串拼接方式的性能差异:

import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Thread)
public class StringConcatBenchmark {

    private String str1 = "Hello";
    private String str2 = "World";
    private String str3 = "JMH";
    private String str4 = "Benchmark";

    @Benchmark
    public String concatWithPlus() {
        return str1 + str2 + str3 + str4;
    }

    @Benchmark
    public String concatWithStringBuilder() {
        StringBuilder sb = new StringBuilder();
        sb.append(str1);
        sb.append(str2);
        sb.append(str3);
        sb.append(str4);
        return sb.toString();
    }

    @Benchmark
    public String concatWithStringBuffer() {
        StringBuffer sb = new StringBuffer();
        sb.append(str1);
        sb.append(str2);
        sb.append(str3);
        sb.append(str4);
        return sb.toString();
    }

    @Benchmark
    public String concatWithStringFormat() {
        return String.format("%s%s%s%s", str1, str2, str3, str4);
    }
}

运行结果可能如下:

Benchmark                              Mode  Cnt      Score      Error  Units
StringConcatBenchmark.concatWithPlus           thrpt    5  12345.678 ±  234.567  ops/ms
StringConcatBenchmark.concatWithStringBuilder  thrpt    5  23456.789 ±  345.678  ops/ms
StringConcatBenchmark.concatWithStringBuffer   thrpt    5  12345.678 ±  123.456  ops/ms
StringConcatBenchmark.concatWithStringFormat   thrpt    5   1234.567 ±   45.678  ops/ms

从结果可以看出:

  1. StringBuilder性能最好
  2. +操作符在现代Java版本中会被优化为StringBuilder,性能也不错
  3. StringBuffer由于同步开销,性能稍差
  4. String.format性能最差

🚀 高级特性

1. 参数化基准测试

使用@Param注解可以测试不同参数下的性能:

@State(Scope.Thread)
public class ParamBenchmark {
    
    @Param({"10", "100", "1000"})
    public int length;
    
    private String str;
    
    @Setup
    public void setup() {
        str = new String(new char[length]).replace('\0', 'x');
    }
    
    @Benchmark
    public String testToUpperCase() {
        return str.toUpperCase();
    }
}

2. 分组测试

可以分组比较不同实现:

@Benchmark
@Group("stringCompare")
public boolean compareWithEquals() {
    return str1.equals(str2);
}

@Benchmark
@Group("stringCompare")
public boolean compareWithEqualsIgnoreCase() {
    return str1.equalsIgnoreCase(str2);
}

3. 时间控制

精确控制测试时间:

@Benchmark
@BenchmarkMode(Mode.SingleShotTime)
@Measurement(batchSize = 10000) // 每轮执行10000次
public void testMethod() {
    // ...
}

📌 总结

  1. JMH是Java微基准测试的事实标准工具,能提供科学准确的性能数据
  2. 基准测试需要考虑JVM预热、编译器优化等多种因素
  3. 使用@State管理测试状态,避免常量折叠等问题
  4. 使用Blackhole防止死代码消除
  5. 不要手动写循环,JMH会自动处理迭代
  6. 实际项目中,应该针对关键路径进行基准测试
  7. 测试结果要结合具体场景分析,没有绝对的"最优"

通过本文的学习,你应该已经掌握了JMH的基本用法和核心概念。在实际项目中,合理使用JMH可以帮助你做出更科学的性能优化决策,避免过早优化和错误优化。

记住:基准测试只是工具,真正的性能优化应该基于实际业务场景和性能分析!


🔔 最后提醒:性能优化应该遵循"三原则":

  1. 先测量,再优化
  2. 优化关键路径
  3. 保持代码可读性

Happy benchmarking! 🎉

推荐阅读文章

  • 由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)

  • 如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系

  • HTTP、HTTPS、Cookie 和 Session 之间的关系

  • 什么是 Cookie?简单介绍与使用方法

  • 什么是 Session?如何应用?

  • 使用 Spring 框架构建 MVC 应用程序:初学者教程

  • 有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

  • 如何理解应用 Java 多线程与并发编程?

  • 把握Java泛型的艺术:协变、逆变与不可变性一网打尽

  • Java Spring 中常用的 @PostConstruct 注解使用总结

  • 如何理解线程安全这个概念?

  • 理解 Java 桥接方法

  • Spring 整合嵌入式 Tomcat 容器

  • Tomcat 如何加载 SpringMVC 组件

  • “在什么情况下类需要实现 Serializable,什么情况下又不需要(一)?”

  • “避免序列化灾难:掌握实现 Serializable 的真相!(二)”

  • 如何自定义一个自己的 Spring Boot Starter 组件(从入门到实践)

  • 解密 Redis:如何通过 IO 多路复用征服高并发挑战!

  • 线程 vs 虚拟线程:深入理解及区别

  • 深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别

  • 10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!

  • “打破重复代码的魔咒:使用 Function 接口在 Java 8 中实现优雅重构!”

  • Java 中消除 If-else 技巧总结

  • 线程池的核心参数配置(仅供参考)

  • 【人工智能】聊聊Transformer,深度学习的一股清流(13)

  • Java 枚举的几个常用技巧,你可以试着用用

  • 由 Spring 静态注入引发的一个线上T0级别事故(真的以后得避坑)

  • 如何理解 HTTP 是无状态的,以及它与 Cookie 和 Session 之间的联系

  • HTTP、HTTPS、Cookie 和 Session 之间的关系

  • 使用 Spring 框架构建 MVC 应用程序:初学者教程

  • 有缺陷的 Java 代码:Java 开发人员最常犯的 10 大错误

  • Java Spring 中常用的 @PostConstruct 注解使用总结

  • 线程 vs 虚拟线程:深入理解及区别

  • 深度解读 JDK 8、JDK 11、JDK 17 和 JDK 21 的区别

  • 10大程序员提升代码优雅度的必杀技,瞬间让你成为团队宠儿!

  • 探索 Lombok 的 @Builder 和 @SuperBuilder:避坑指南(一)

  • 为什么用了 @Builder 反而报错?深入理解 Lombok 的“暗坑”与解决方案(二)

相关文章:

  • sqlite3基本语句
  • BUUCTF-web刷题篇(17)
  • Three.js 入门实战:安装、基础概念与第一个场景⭐
  • go语言应该如何学习
  • SQL:JOIN 完全指南:从基础到实战应用
  • EFA-YOLO:一种高效轻量的火焰检测模型解析
  • 【期中准备】电路基础(西电)
  • MySQL事务管理
  • 3 版本控制:GitLab、Jenkins 工作流及分支开发模式实践
  • Kubernetes 深入浅出系列 | 容器剖析之容器安全
  • 链路聚合+vrrp
  • 写给新人的深度学习扫盲贴:ReLu和梯度
  • DocLayout-YOLO:通过多样化合成数据与全局-局部感知实现文档布局分析突破
  • 【Java内存区域有什么?每个区域有什么作用?】
  • 跨站脚本攻击(XSS)与跨站请求伪造(CSRF)的介绍、区别和预防
  • 程序化广告行业(74/89):行业发展驱动因素与未来展望
  • 帆软fvs文件中某表格新增数据来声提醒
  • Kotlin日常使用函数记录
  • JavaScript逆向工程实战:如何精准定位加密参数生成位置
  • 大模型学习七:‌小米8闲置,直接安装ubuntu,并安装VNC远程连接手机,使劲造
  • 做网站端口内容无法替换/小说网站排名前十
  • 哪里能找到免费网站/百度小说排行榜前十名
  • 上城网站建设/自己如何开网站
  • 福州建网站/公司网站模版
  • 有什么网站是可以做动态图的/职业技能培训班
  • 重庆做网站建设公司排名/深圳刚刚突然宣布