如何测量Java代码的性能
在 Java 中,可以使用多种方法来测量一段代码的执行性能。使用 System.currentTimeMillis()是最常见的方法
long startTime = System.currentTimeMillis();// 需要测量的代码块
for (int i = 0; i < 1000000; i++) {// 示例代码
}long endTime = System.currentTimeMillis();
long duration = endTime - startTime; // 执行时间(毫秒)
System.out.println("Execution time: " + duration + " ms");
但是这么测结果是不太准确的,因为Java存在代码优化以及JIT编译等等,通常更准确的方式是使用Benchmark的方式来评估性能。
JMH基本使用
参考:https://github.com/openjdk/jmh
Java Microbenchmark Harness (JMH) 是一个用于基准测试 Java 代码的工具。它能够准确测量代码的性能,帮助开发者了解不同实现的效率。
首先添加依赖,jmh一般用于test,所以scope可以指定为test
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>1.35</version>
</dependency>
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.35</version>
</dependency>
示例如下:
package org.example;import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;import java.util.Arrays;
import java.util.concurrent.TimeUnit;@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class JMHExample {private int[] numbers;public static void main(String[] args) throws Exception {Options opts = new OptionsBuilder().include(JMHExample.class.getSimpleName()) // 指定测试的类.build();new Runner(opts).run(); // 运行}@Setup(Level.Trial)public void setup() {numbers = new int[1000];for (int i = 0; i < numbers.length; i++) {numbers[i] = i;}}@Benchmarkpublic int sumArray() {int sum = 0;for (int number : numbers) {sum += number;}return sum;}@Benchmarkpublic int sumArrayParallel() {return Arrays.stream(numbers).parallel().sum();}
}
在示例中,其实我们的目的就是比较两种对数组求和方式的性能,看看每秒的吞吐量如何,运行测试,可以在控制台得到相应输出。
常用注解
@Benchmark
@Benchmark 注解是用于标记测试方法的,类似 JUnit 中的 @Test 注解需要单元测试的方法一样,只有被这个注解标记的方法才会参与基准测试,且被标记的方法必须是 public 的。在一个基本测试类中至少包含一个被 @Benchmark 标记的方法,否则会抛出异常。
@BenchmarkMode
@BenchmarkMode 注解用于指定基准测试的模式。JMH 共有四种模式:
- Throughput:整体吞吐量,例如“1 秒内可以执行多少次调用”。
- AverageTime:调用的平均时间,例如“每次调用平均耗时 xxx 毫秒”。如果需要测试某个方法的平均耗时,可以使用@BenchmarkMode 注解并指定基准测试的模式为 AverageTime。
- SampleTime:随机取样,最后输出取样结果的分布,例如“99%的调用在 xxx 毫秒以内,99.99%的调用在 xxx 毫秒以内”。
- SingleShotTime:以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为 0,用于测试冷启动时的性能。
- All:所有的指标全算一遍
在使用时,@BenchmarkMode 注解可设置在类上也可以设置在基准方法上。例如:
@Benchmark
@BenchmarkMode(Mode.AverageTime)
public void methodToTest() {// 测试代码
}
@OutputTimeUnit
基准测试结果的时间类型。一般选择秒、毫秒、微秒,这里填入的是 TimeUnit 这个枚举类型,涉及单位很多从纳秒到天都有,按需选择,最终输出易读的结果。
@State
@State 指定了在类中变量的作用范围。@State 用于声明某个类是一个“状态”,可以用Scope 参数用来表示该状态的共享范围。这个注解必须加在类上,否则提示无法运行。它有三个取值。
- Benchmark:表示变量的作用范围是某个基准测试类。
- Thread:每个线程一份副本,如果配置了Threads注解,则每个Thread都拥有一份变量,它们互不影响。
- Group:联系上面的@Group注解,在同一个Group里,将会共享同一个变量实例。
本例中,相关变量的作用范围是 Benchmark。
@Warmup
预热,可以加在类上或者方法上,预热只是测试数据,是不作为测量结果的。
该注解一共有4个参数:
- iterations 预热阶段的迭代数
- time 每次预热时间
- timeUnit 时间单位,通常秒
- batchSize 批处理大小,指定每次操作调用几次方法
本例中,我们加在类上,让它迭代3次,每次1秒,时间单位秒。
@Setup
注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的,这个也和Junit的@Before等类似
@Teardown
在测试之后进行一些结束工作,主要用于资源回收
@Measurement
和预热类似,这个注解是会影响测试结果的,它的参数和 Warmup 一样,这里不多介绍。
本例中我们在迭代中设置的是5次,每次1秒。
通常 @Warmup 和 @Measurement 两个参数会一起使用。
@Fork
表示开启几个进程测试,通常我们设为1,如果数值大于1,则启用新的进程测试,如果设置为0,程序依然进行,但是在用户的 JVM 进程上运行。
@Threads
上面的注解注重开启几个进程,这里就是开启几个线程,只有一个参数 value,指定注解的value,将会开启并行测试,如果设置的 value 过大,如 Threads.Max,则使用处理机的相同线程数。
输出格式
public static void main(String[] args) throws RunnerException {Options opts = new OptionsBuilder()// 表示包含的测试类.include(JMHExample.class.getSimpleName()) // 最后结果输出文件的命名,不指定默认为jmh-reuslt.json.result("benchmark.json")// 结果输出什么格式,可以是json, csv, text等.resultFormat(ResultFormatType.JSON).build();new Runner(opts).run(); // 运行
}
作为程序开发人员,看懂测试结果没难度,测试结果文本能可视化更好。好在我们拿到了JMH 结果后,根据文件格式,我们可以二次加工,就可以图表化展示[2]。
JMH 支持的几种输出格式:
- TEXT 导出文本文件。
- CSV 导出csv格式文件。
- SCSV 导出scsv等格式的文件。
- JSON 导出成json文件。
- LATEX 导出到latex,一种基于ΤΕΧ的排版系统。
比如 CSV 格式的文件,我们就可以通过 EXCEL 处理获取图表,当然也还有其他的一些工具,例如:
- https://jmh.morethan.io/,参考:https://github.com/jzillmann/jmh-visualizer
这个网站要使用json格式的输出结果
jmh-generator
JMH生成测试代码的方式有多种,例如jmh-generator-annprocess
和 jmh-generator-reflection
是 JMH(Java Microbenchmark Harness)中用于基准测试生成的两个核心组件。
- jmh-generator-annprocess:这个模块用于在编译时处理基准测试的注解。它通过注解处理器生成相应的基准测试代码。
-
性能:由于是在编译时生成代码,运行时的开销较小,因此性能较好。
-
类型安全:在编译时生成的代码是类型安全的,可以捕获潜在的错误。
使用场景:适合于需要大量基准测试并且希望在编译时进行优化的场景。
- jmh-generator-reflection:这个模块使用反射机制在运行时生成基准测试代码,在运行时读取类的结构,并生成相应的基准测试实现。
-
优点
- 灵活性:可以动态生成基准测试,适合那些在编译时无法确定的基准测试场景。
- 简化:对于简单或快速的基准测试,开发者无需进行额外的编译配置。
-
缺点
- 性能开销:由于使用反射,运行时性能开销相对较大。
- 类型安全:可能导致运行时错误,缺乏编译时检查。
如果需要高性能、类型安全的基准测试,并且可以接受编译时的复杂性。选择 jmh-generator-annprocess,如果希望动态生成基准测试,或者在快速原型开发时需要更灵活的解决方案。选择 jmh-generator-reflection
对应的maven依赖如下
参考:https://mvnrepository.com/artifact/org.openjdk.jmh
<!-- 基于注解处理器生成 -->
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>1.37</version><scope>test</scope>
</dependency>
<!-- 基于反射生成 -->
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-reflection</artifactId><version>1.37</version><scope>test</scope>
</dependency>
<!-- 基于字节码生成 -->
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-bytecode</artifactId><version>1.37</version><scope>test</scope>
</dependency>