JMH:让你的Java程序性能翻倍的神器

大家好!今天我要向大家详细介绍JMH(Java Microbenchmark Harness),这个被誉为Java性能测试的利器。无论你是想优化现有的Java代码还是开发新的项目,JMH都能够帮助你准确、可靠地测量和分析代码的性能,让我们一起来探索JMH的神奇之处吧!

一、JMH 简 介

JMH是由OpenJDK团队开发的一款专业的基准测试工具,旨在提供一个可靠的测试框架,帮助Java开发者进行代码性能的评估和优化。

JMH能够对代码进行微基准测试,以提供精确的性能数据,并帮助开发者发现潜在的性能问题。

二、为 什 么 使 用 JMH 

  1. 高度可靠:JMH采用严格的度量方式,能够排除外部干扰因素的影响,提供准确的性能数据。

  2. 直观可视化:JMH提供丰富的测试结果图表和报告生成工具,让性能数据一目了然,方便开发者分析和优化。

  3. 灵活的配置选项:JMH提供多种注解和选项,可以根据需求进行灵活配置,满足不同类型的性能测试需求。

  4. 自动优化:JMH在测试执行过程中会使用Just-In-Time(JIT)编译器进行代码优化,确保测试结果准确无误。

三、JMH 的 使 用 步 骤

  1. 引入JMH依赖:在项目的构建工具中,引入JMH的依赖,例如Maven或Gradle等。【jdk1.8以下,,包括1.8, 1.9+自带】

<dependencies>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-core</artifactId>
        <version>1.23</version>
    </dependency>
    <dependency>
        <groupId>org.openjdk.jmh</groupId>
        <artifactId>jmh-generator-annprocess</artifactId>
        <version>1.23</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

  2. 编写待测试的方法:使用@Benchmark注解标记待测试的方法,确保方法的可重复执行性。

public class MyBenchmark {
    @Benchmark
    public void myMethod() {
        // 待测试的代码逻辑
    }
}
  1. 配置测试选项:使用各种注解来配置基准测试的参数,例如@BenchmarkMode、@Warmup、@Measurement等。

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class MyBenchmark {
    @Benchmark
    public void myMethod() {
        // 待测试的代码逻辑
    }
}
  1. 运行基准测试:编写入口方法,通过mian方法来运行基准测试,生成测试结果。

@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class MyBenchmark {
    @Benchmark
    public void myMethod() {
        // 待测试的代码逻辑
    }

    @Benchmark
    public void myMethod1() {
        // 待测试的代码逻辑
    }

    public static void main(String[] args) throws RunnerException {
    Options options = new OptionsBuilder()
            .include(MyBenchmark.class.getSimpleName())
            .forks(1)
            .build();

    new Runner(options).run();
}
}

四、常 用 JMH 注 解 和 选 项

  1. @Benchmark:标记待测试的方法,JMH会自动执行和测量该方法。

  2.  @BenchmarkMode:设置基准测试的模式,默认为Mode.Throughput,还有Mode.AverageTime、Mode.SampleTime等模式可选。

  1. @Warmup:设置预热的迭代次数和时间,用于避免测试开始时的JIT编译影响。

  2. @Measurement:设置测量的迭代次数和时间,用于最终的性能测量。

  3. @Fork:设置进行测试的进程数,可以通过多次运行取得平均结果。

  4. @State:定义测试状态类,可以在不同的基准测试方法之间共享状态。

  5. @Setup:注解可以用于初始化操作,我们可以在基准测试执行前进行一些准备工作。

  6. @TearDown: 注解可以用于清理操作,我们可以在基准测试执行后对资源进行释放。

  7. @Param:用于参数化测试,给定一组参数来运行相同的测试方法。


@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Fork(2)
public class MyBenchmark {

    @Param({"value1", "value2", "value3"})
    public String param;

    @Benchmark
    public void myMethod() {
        // 待测试的代码逻辑
    }

    @Setup
    public void setup() {
        // 进行基准测试前的初始化操作
    }

    @TearDown
    public void teardown() {
        // 进行基准测试后的清理操作
    }
}

上述代码中,我们使用了@State注解来定义了一个测试状态类,以便在不同的基准测试方法间共享状态。

@BenchmarkMode设置了基准测试的模式为吞吐量模式。

@Warmup注解表示预热阶段的迭代次数为3次,每次迭代1秒。

@Measurement注解表示正式性能测试的迭代次数为5次,每次迭代1秒。

@Fork注解表示执行两次测试进程。

在类中,我们定义了一个被@Benchmark注解标记的方法myMethod(),这是我们待测试的方法。

使用@Param注解对param参数进行了参数化测试。

@Setup注解表示在执行基准测试之前进行的初始化操作,

@TearDown注解表示在执行基准测试之后进行的清理操作。

四、案例:比较String 跟 StringBulider 拼接字符串执行效率


/**
 * @author: xrp
 * @date: 2023/10/09/14:11
 * @description
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations = 4, time = 2)
@Measurement(iterations = 4, time = 5)
@State(Scope.Thread)
@Slf4j
public class JmhTest {


    ServerInfo serverInfo;
    ServerVO finalServerVO;
    EsLoginMonitorRecord esLoginMonitorRecord;

    @Setup
    public void setApplicationContext() {
   
        serverInfo = new ServerInfo();
        serverInfo.setServerId(1);
        serverInfo.setHostName("123");
        serverInfo.setMacCode("123");

        finalServerVO  = new ServerVO();
        finalServerVO.setGroupStr("1");
        finalServerVO.setTagName("123");

        esLoginMonitorRecord = new EsLoginMonitorRecord();
        esLoginMonitorRecord.setUserName("123");
        esLoginMonitorRecord.setType("1232");
        esLoginMonitorRecord.setStatus("123");
        esLoginMonitorRecord.setStatus("123");
        esLoginMonitorRecord.setLoginIp("123");
        esLoginMonitorRecord.setLoginTime("122");
        esLoginMonitorRecord.setCountry("123");
        esLoginMonitorRecord.setProvince("1236");
        esLoginMonitorRecord.setCity("1568");
    }



    @Benchmark
    public void testString() {
        String  result = "";
        for (int i = 0; i < 2000 ; i++) {
            result += "serverId/"+serverInfo.getServerId()
                    +",serverIp/"+serverInfo.getServerIp()
                    +",hostName/"+serverInfo.getHostName()
                    +",macCode/"+serverInfo.getMacCode()
                    +",groupName/"+StringUtils.defaultString(finalServerVO.getGroupStr())
                    +",tagName/"+StringUtils.defaultString(finalServerVO.getTagName())
                    +",hostRemark/"+StringUtils.defaultString(serverInfo.getRemark())
                    +",userName/"+esLoginMonitorRecord.getUserName()
                    +",type/"+esLoginMonitorRecord.getType()
                    +",status/"+esLoginMonitorRecord.getStatus()
                    +",loginIp/"+esLoginMonitorRecord.getLoginIp()
                    +",loginTime/"+esLoginMonitorRecord.getLoginTime()
                    +",country/"+esLoginMonitorRecord.getCountry()
                    +",province/"+esLoginMonitorRecord.getProvince()
                    +",city/"+esLoginMonitorRecord.getCity();
        }
    }
    

    @Benchmark
    public void testStringBuilder() {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < 2000 ; i++) {
            sb.append("serverId/").append(serverInfo.getServerId())
                    .append(",serverIp/").append(serverInfo.getServerIp()).append("(").append(serverInfo.getServerLocalIp()).append(")")
                    .append(",hostName/").append(serverInfo.getHostName())
                    .append(",macCode/").append(serverInfo.getMacCode())
                    .append(",groupName/").append(StringUtils.defaultString(finalServerVO.getGroupStr()))
                    .append(",tagName/").append(StringUtils.defaultString(finalServerVO.getTagName()))
                    .append(",hostRemark/").append(StringUtils.defaultString(serverInfo.getRemark()))
                    .append(",userName/").append(esLoginMonitorRecord.getUserName())
                    .append(",type/").append(esLoginMonitorRecord.getType())
                    .append(",status/").append(esLoginMonitorRecord.getStatus())
                    .append(",loginIp/").append(esLoginMonitorRecord.getLoginIp())
                    .append(",loginTime/").append(esLoginMonitorRecord.getLoginTime())
                    .append(",country/").append(esLoginMonitorRecord.getCountry())
                    .append(",province/").append(esLoginMonitorRecord.getProvince())
                    .append(",city/").append(esLoginMonitorRecord.getCity());
        }
    }

    public static void main(String[] args) throws RunnerException {
        Options optionsBuilder = new OptionsBuilder()
                .include(JmhTest.class.getSimpleName())
                .result("L:\\1.json")
                .resultFormat(ResultFormatType.JSON)
                .forks(1)
                .build();
        new Runner(optionsBuilder).run();
    }
}

在案例中可以看到先用了 @Setup 注解初始化了一些数据 提供测试类使用 @BenchmarkMode(Mode.AverageTime)  采用的是计算平均耗时模式,单位毫秒 @OutputTimeUnit(TimeUnit.MILLISECONDS)   看看输出结果

在结果输出方式中,我增加了一个json文件进行输出数据,我们可以把json文件上传到分析网站,可以更加直观的得出结论

分析网站 http://deepoove.com/jmh-visual-chart/ 

可以看出,使用String进行直接拼接大量字符串性能是非常差的,这就是为什么要求使用StringBuilder的原因

六、总结

JMH是一款功能强大的Java性能测试工具,它能够帮助开发者准确、可靠地评估代码的性能,并为性能优化提供参考。

它的使用简单灵活,提供了丰富的注解和选项供开发者配置。

通过使用JMH,我们可以更好地了解代码的性能瓶颈,并进行相应的优化。让我们拿起JMH这个“性能宝剑”,驰骋在Java的性能战场,开创出更优秀的Java应用!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值