测试JAVA中的方法的执行性能,比较稳妥合理的方法,是用JMH(https://openjdk.java.net/projects/code-tools/jmh/)
这个JAVA的测试工具。
1)MAVEN加入库:
1.21
org.openjdk.jmh
jmh-core
${jmh.version}
org.openjdk.jmh
jmh-generator-annprocess
${jmh.version}
2 下面是测试下JDK中一些常见的循环迭代等的性能例子,比如:
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
@Fork(value = 2, jvmArgs = {"-Xms2G", "-Xmx2G"})
//@Warmup(iterations = 3)
//@Measurement(iterations = 8)
public class BenchmarkLoop {
@Param({"10000000"})
private int N;
private List DATA_FOR_TESTING;
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(BenchmarkLoop.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
@Setup
public void setup() {
DATA_FOR_TESTING = createData();
}
@Benchmark
public void loopFor(Blackhole bh) {
for (int i = 0; i < DATA_FOR_TESTING.size(); i++) {
String s = DATA_FOR_TESTING.get(i); //take out n consume, fair with foreach
bh.consume(s);
}
}
@Benchmark
public void loopWhile(Blackhole bh) {
int i = 0;
while (i < DATA_FOR_TESTING.size()) {
String s = DATA_FOR_TESTING.get(i);
bh.consume(s);
i++;
}
}
@Benchmark
public void loopForEach(Blackhole bh) {
for (String s : DATA_FOR_TESTING) {
bh.consume(s);
}
}
@Benchmark
public void loopIterator(Blackhole bh) {
Iterator iterator = DATA_FOR_TESTING.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
bh.consume(s);
}
}
private List createData() {
List data = new ArrayList<>();
for (int i = 0; i < N; i++) {
data.add("Number : " + i);
}
return data;
}
}
下面讲解下相关的注解:
Mode 表示 JMH 进行 Benchmark 时所使用的模式。通常是测量的维度不同,或是测量的方式不同。目前 JMH 共有四种模式:
输出的时间单位。
Iteration 是 JMH 进行测试的最小单位。在大部分模式下,一次 iteration 代表的是一秒,JMH 会在这一秒内不断调用需要 Benchmark 的方法,然后根据模式对其采样,计算吞吐量,计算平均执行时间等。
Warmup 是指在实际进行 Benchmark 前先进行预热的行为。
为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 Benchmark 的结果更加接近真实情况就需要进行预热。
类注解,JMH测试类必须使用 @State 注解,它定义了一个类实例的生命周期,可以类比 Spring Bean 的 Scope。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:
进行 fork 的次数。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
提供真正的测试阶段参数。指定迭代的次数,每次迭代的运行时间和每次迭代测试调用的数量(通常使用 @BenchmarkMode(Mode.SingleShotTime) 测试一组操作的开销——而不使用循环)
方法注解,会在执行 benchmark 之前被执行,正如其名,主要用于初始化。
方法注解,与@Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。
@Setup/@TearDown注解使用Level参数来指定何时调用fixture:
名称 | 描述 | |
---|---|---|
Level.Trial | 默认level。全部benchmark运行(一组迭代)之前/之后 | |
Level.Iteration | 一次迭代之前/之后(一组调用) | |
Level.Invocation | 每个方法调用之前/之后(不推荐使用,除非你清楚这样做的目的) |
方法注解,表示该方法是需要进行 benchmark 的对象。
成员注解,可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。@Param 注解接收一个String数组,在 @Setup 方法执行前转化为为对应的数据类型。多个 @Param 注解的成员之间是乘积关系,譬如有两个用 @Param 注解的字段,第一个有5个值,第二个字段有2个值,那么每个测试方法会跑5*2=10次。
前面提到测试的类型是吞吐量,也就是一秒钟调用完成的次数,但是如果想知道做一次需要多少时间该怎么办?
其实 1 / 吞吐量 就是这个值
JMH 提供了以下几种类型进行支持:
类型 | 描述 |
---|---|
Throughput | 每段时间执行的次数,一般是秒 |
AverageTime | 平均时间,每次操作的平均耗时 |
SampleTime | 在测试中,随机进行采样执行的时间 |
SingleShotTime | 在每次执行中计算耗时 |
All | 顾名思义,所有模式,这个在内部测试中常用 |
也可以用MAVEN的方式运行:
org.apache.maven.plugins
maven-shade-plugin
3.2.0
package
shade
benchmarks
org.openjdk.jmh.Main
$ mvn package
$ java -jar target\benchmarks.jar BenchmarkLoop
运行后结果:
Benchmark (N) Mode Cnt Score Error Units
BenchmarkLoop.loopFor 10000000 avgt 10 61.673 ± 1.251 ms/op
BenchmarkLoop.loopForEach 10000000 avgt 10 67.582 ± 1.034 ms/op
BenchmarkLoop.loopIterator 10000000 avgt 10 66.087 ± 1.534 ms/op
BenchmarkLoop.loopWhile 10000000 avgt 10 60.660 ± 0.279 ms/op
可以看出其SCORE的分数了。