一、JMH是什么?
JMH是什么?
JMH是Java性能测试工具,主要是对工程中一些方法进行一些基准测试,支持的时间单位为:nano / micro / milli / macro
二、JMH的Jar包
Java项目开始编写JMH实例时肯定要先知道引用的哪些Jar包,JMH使用引用Jar包如下:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.19</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.19</version>
<scope>provided</scope>
</dependency>
--------------------------------------------------------------------------------------------------------
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>1.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>microbenchmarks</finalName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
三、JMH的注解解释
@BenchmarkMode
基准测试类型。这里选择的是Throughput也就是吞吐量。根据源码点进去,每种类型后面都有对应的解释,比较好理解,吞吐量会得到单位时间内可以进行的操作数。
Throughput: 整体吞吐量,例如“1秒内可以执行多少次调用”。
AverageTime: 调用的平均时间,例如“每次调用平均耗时xxx毫秒”。
SampleTime: 随机取样,最后输出取样结果的分布,例如“99%的调用在xxx毫秒以内,99.99%的调用在xxx毫秒以内”
SingleShotTime: 以上模式都是默认一次 iteration 是 1s,唯有 SingleShotTime 是只运行一次。往往同时把 warmup 次数设为0,用于测试冷启动时的性能。
All(“all”, “All benchmark modes”);
@Warmup
上面我们提到了,进行基准测试前需要进行预热。一般我们前几次进行程序测试的时候都会比较慢, 所以要让程序进行几轮预热,保证测试的准确性。其中的参数iterations也就非常好理解了,就是预热轮数。
为什么需要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
iterations:预热的次数。
time:每次预热的时间。
timeUnit:时间的单位,默认秒。
batchSize:批处理大小,每次操作调用几次方法。
@Measurement
度量,其实就是一些基本的测试参数。
iterations 进行测试的轮次
time 每轮进行的时长
timeUnit 时长单位
都是一些基本的参数,可以根据具体情况调整。一般比较重的东西可以进行大量的测试,放到服务器上运行。
@Threads
每个进程中的测试线程,可用于类或者方法上。一般选择为cpu乘以2。如果配置了 Threads.MAX ,代表使用 Runtime.getRuntime().availableProcessors() 个线程。
@Fork
进行 fork 的次数。可用于类或者方法上。如果 fork 数是2的话,则 JMH 会 fork 出两个进程来进行测试。
@OutputTimeUnit
这个比较简单了,基准测试结果的时间类型。一般选择秒、毫秒、微秒。
@Benchmark
方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。
@Param
属性级注解,@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。
@Setup
方法级注解,这个注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的。
@TearDown
方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。
@Setup主要实现测试前的初始化工作,只能作用在方法上。用法和Junit一样。使用该注解必须定义 @State注解。
@TearDown主要实现测试完成后的垃圾回收等工作,只能作用在方法上。用法和Junit一样。使用该注解必须定义 @State 注解。
这两个注解都有一个 Level 的枚举value,它有三个值(默认的是Trial):
Trial:在每次Benchmark的之前/之后执行。
Iteration:在每次Benchmark的iteration的之前/之后执行。
Invocation:每次调用Benchmark标记的方法之前/之后都会执行。
可见,Level的粒度从Trial到Invocation越来越细。
@State
当使用@Setup参数的时候,必须在类上加这个参数,不然会提示无法运行。
State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。 因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。
Thread: 该状态为每个线程独享。
Group: 该状态为同一个组里面所有线程共享。
Benchmark: 该状态在所有线程间共享。
四、开始实例对比单线程和多线程(代码实例)
这个例子是:一个1亿大小的 int 类型数组 里面全存的都是1,
多线程模式下:将这个数组分为四段,用四个线程去进行累加,最后再将每个线程的和相加取得和值
单线程模式下: 直接去遍历从0到1亿进行累加
最后比较单线程和多线程所执行的时间
代码如下:
/**
* JMH测试单线程和多线程使用的时间
* */
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3)
@Measurement(iterations = 10)
public class MyBenchmark {
static int[] ARRAY = new int[1000_000_00];
static {
Arrays.fill(ARRAY,1);
}
@Benchmark
public int c() throws ExecutionException, InterruptedException {
int[] array = ARRAY;
FutureTask<Integer> t1 = new FutureTask<>(() ->{
int sum = 0;
for (int i=0; i<25000000 ; i++) {
sum += array[0+i];
}
return sum;
});
FutureTask<Integer> t2 = new FutureTask<>(() -> {
int sum = 0;
for (int i=25000000; i<50000000;i++){
sum += array[0+i];
}
return sum;
});
FutureTask<Integer> t3 = new FutureTask<>(() -> {
int sum = 0;
for (int i=50000000; i<75000000;i++){
sum += array[0+i];
}
return sum;
});
FutureTask<Integer> t4 = new FutureTask<>(() -> {
int sum = 0;
for (int i=75000000; i<100000000;i++){
sum += array[0+i];
}
return sum;
});
new Thread(t1).start();
new Thread(t2).start();
new Thread(t3).start();
new Thread(t4).start();
return t1.get() + t2.get() + t3.get() + t4.get();
}
@Benchmark
public int d() throws ExecutionException, InterruptedException {
int[] array = ARRAY;
FutureTask<Integer> t1 = new FutureTask<>(() ->{
int sum = 0;
for (int i=0; i<100000000 ; i++) {
sum += array[0+i];
}
return sum;
});
new Thread(t1).start();
return t1.get();
}
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
}
五、运行结果
上面结果
c方法是多线程
d方法是单线程
一目了然可得知在多核CPU下,多线程的运行速度,比单线程快
但是在单核CPU下情况不是很确定,因为多线程有上下文切换,所有可以使用虚拟机去进行测试