JMH
JMH(Java Microbenchmark Harness)是由OpenJDK Developer提供的基准测试工具(基准可以理解为比较的基础,我们将这一次性能测试结果作为基准结果,下一次的测试结果将与基准数据进行比较),它是一种常用的性能测试工具,解决了基准测试中常见的一些问题。
结果日志解释
基础信息,显示Java路径、Java版本以及JMH基础配置信息
预热次数。预热测试不会作为最终的统计结果。预热的目的是让 JVM 对被测代码进行足够多的优化 ,被测代码应该得到了充分的 JIT 编译和优化。
1 2 3 4 5 Iteration 1: 0.002 ±(99.9%) 0.001 ms/op Result "cn.z201.jmh.ListBenchmark.testArrayList": 0.002 ms/op
结果表明,在拼接字符次数越多的情况下,LinkedList.add()
的性能就更好。这是得益于LinkedList双向链表结构,每次add都是在最后一个位置添加元素。
1 2 3 4 5 6 7 8 9 10 Benchmark (size) Mode Cnt Score Error Units ListBenchmark.testArrayList 100 avgt 0.002 ms/op ListBenchmark.testArrayList 1000 avgt 0.018 ms/op ListBenchmark.testArrayList 10000 avgt 0.194 ms/op ListBenchmark.testCopyOnWriteArrayList 100 avgt 0.015 ms/op ListBenchmark.testCopyOnWriteArrayList 1000 avgt 1.386 ms/op ListBenchmark.testCopyOnWriteArrayList 10000 avgt 148.161 ms/op ListBenchmark.testLinkedList 100 avgt 0.002 ms/op ListBenchmark.testLinkedList 1000 avgt 0.017 ms/op ListBenchmark.testLinkedList 10000 avgt 0.163 ms/op
注解使用 @BenchmarkMode 用来配置 Mode 选项,可用于类或者方法上,这个注解的 value 是一个数组,可以把几种 Mode 集合在一起执行,如:@BenchmarkMode({Mode.SampleTime, Mode.AverageTime})
,还可以设置为 Mode.All
,即全部执行一遍。
Throughput:整体吞吐量,每秒执行了多少次调用,单位为 ops/time
AverageTime:用的平均时间,每次操作的平均时间,单位为 time/op
SampleTime:随机取样,最后输出取样结果的分布
SingleShotTime:只运行一次,往往同时把 Warmup 次数设为 0,用于测试冷启动时的性能
All:上面的所有模式都执行一次
@State 通过 State 可以指定一个对象的作用范围,JMH 根据 scope 来进行实例化和共享操作。@State 可以被继承使用,如果父类定义了该注解,子类则无需定义。由于 JMH 允许多线程同时执行测试,不同的选项含义如下:
Scope.Benchmark:所有测试线程共享一个实例,测试有状态实例在多线程共享下的性能
Scope.Group:同一个线程在同一个 group 里共享实例
Scope.Thread:默认的 State,每个测试线程分配一个实例
@OutputTimeUnit
@Warmup 预热所需要配置的一些基本测试参数,可用于类或者方法上。一般前几次进行程序测试的时候都会比较慢,所以要让程序进行几轮预热,保证测试的准确性。参数如下所示:
JVM 的 JIT 机制,如果某个函数被调用多次之后,JVM 会尝试将其编译为机器码,从而提高执行速度,所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
@Measurement
实际调用方法所需要配置的一些基本测试参数,可用于类或者方法上,参数和 @Warmup
相同。
@Threads
@Fork
进行 fork 的次数,可用于类或者方法上。如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
@Param
指定某项参数的多种情况,特别适合用来测试一个函数在不同的参数输入的情况下的性能,只能作用在字段上,使用该注解必须定义 @State 注解。
如果只是用@Param
在编译时会报错,它必须配合@State
注解使用,@State
指定了对象共享范围。
@State(value = Scope.Benchmark):基准测试内共享对象
@State(value = Scope.Group):同一个线程组内共享
@State(value = Scope.Thread):同一个线程内共享
@Setup
& @TearDown
假如初始化和销毁代码并不是基准测试的一部分,为了减少测试噪,音所以不应该放到@Benchmark
修饰的方法内部,JMH提供了@Setup
和@TearDown
实现这样的功能。
案例一
比较 String.join、StringBuilder.append 、StringBuffer.append参数
演示代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 package cn.z201.jmh;@State(Scope.Benchmark) public class StringBenchmark { private final static Integer MEASUREMENT_ITERATIONS = 1 ; private final static Integer WARMUP_ITERATIONS = 1 ; @Param(value = {"100","1000","10000"}) private int size; public List<Integer> stringList = null ; @Setup(Level.Trial) public synchronized void initialize () { try { stringList = new Random () .ints() .limit(size) .boxed() .map(i->i.toString()) .collect(Collectors.toList()); } catch (Exception e) { e.printStackTrace(); } } @Test public void executeJmhRunner () throws RunnerException { Options opt = new OptionsBuilder () .include("\\." + StringBenchmark.class.getSimpleName() + "\\." ) .warmupIterations(WARMUP_ITERATIONS) .measurementIterations(MEASUREMENT_ITERATIONS) .timeUnit(TimeUnit.MILLISECONDS) .forks(0 ) .threads(8 ) .mode(Mode.AverageTime) .shouldDoGC(true ) .shouldFailOnError(true ) .resultFormat(ResultFormatType.JSON) .result("benchmark.json" ) .shouldFailOnError(true ) .jvmArgs("-server" ) .build(); new Runner (opt).run(); } @Benchmark public void testStringJoin (Blackhole blackhole) { String str = new String (); stringList.stream().forEach(i-> str.join(i.toString())); blackhole.consume(str); } @Benchmark public void testStringBuffer (Blackhole blackhole) { StringBuffer stringBuffer = new StringBuffer (); stringList.stream().forEach(i-> stringBuffer.append(i.toString())); blackhole.consume(stringBuffer); } @Benchmark public void testStringBuilder (Blackhole blackhole) { StringBuilder stringBuilder = new StringBuilder (); stringList.stream().forEach(i-> stringBuilder.append(i.toString())); blackhole.consume(stringBuilder); } }
案例二
比较ArrayList.add 、LinkedList.add、CopyOnWriteArrayList.add
演示代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 @State(Scope.Benchmark) public class ListBenchmark { private final static Integer MEASUREMENT_ITERATIONS = 1 ; private final static Integer WARMUP_ITERATIONS = 1 ; @Param(value = {"100","1000","10000"}) private int size; public List<String> stringList = null ; @Setup(Level.Trial) public synchronized void initialize () { try { stringList = new Random () .ints() .limit(size) .boxed() .map(i->i.toString()) .collect(Collectors.toList()); } catch (Exception e) { e.printStackTrace(); } } @Test public void executeJmhRunner () throws RunnerException { Options opt = new OptionsBuilder () .include("\\." + getClass().getSimpleName() + "\\." ) .warmupIterations(WARMUP_ITERATIONS) .measurementIterations(MEASUREMENT_ITERATIONS) .timeUnit(TimeUnit.MILLISECONDS) .forks(0 ) .threads(8 ) .mode(Mode.AverageTime) .shouldDoGC(true ) .shouldFailOnError(true ) .resultFormat(ResultFormatType.JSON) .result("ListBenchmark.json" ) .shouldFailOnError(true ) .jvmArgs("-server" ) .build(); new Runner (opt).run(); } @Benchmark public void testArrayList (Blackhole blackhole) { List<String> arrayList = new ArrayList <>(); stringList.stream().forEach(i->arrayList.add(i)); blackhole.consume(arrayList); } @Benchmark public void testLinkedList (Blackhole blackhole) { List<String> linkedList = new LinkedList <>(); stringList.stream().forEach(i->linkedList.add(i)); blackhole.consume(linkedList); } @Benchmark public void testCopyOnWriteArrayList (Blackhole blackhole) { List<String> copyOnWriteArrayList = new CopyOnWriteArrayList <>(); stringList.stream().forEach(i->copyOnWriteArrayList.add(i)); blackhole.consume(copyOnWriteArrayList); } }
案例三
JMH 官方提供了生成 jar 包的方式来执行
对比AtomicLong 、VolatileLong 、log求和
打包项目 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <plugin > <groupId > org.apache.maven.plugins</groupId > <artifactId > maven-shade-plugin</artifactId > <version > 2.4.1</version > <executions > <execution > <phase > package</phase > <goals > <goal > shade</goal > </goals > <configuration > <finalName > jmh</finalName > <transformers > <transformer implementation ="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer" > <mainClass > cn.z201.jmh.Main</mainClass > </transformer > </transformers > </configuration > </execution > </executions > </plugin >
测试代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 @State(Scope.Benchmark) public class Main { private final static Integer MEASUREMENT_ITERATIONS = 1 ; private final static Integer WARMUP_ITERATIONS = 1 ; @Param(value = {"100", "1000", "10000"}) private int size; public List<Integer> numberList = null ; @Setup(Level.Trial) public synchronized void initialize () { try { numberList = new Random () .ints() .limit(size) .boxed() .collect(Collectors.toList()); } catch (Exception e) { e.printStackTrace(); } } public static void main (String[] args) throws RunnerException { Options opt = new OptionsBuilder () .include("\\." + Main.class.getSimpleName() + "\\." ) .warmupIterations(WARMUP_ITERATIONS) .measurementIterations(MEASUREMENT_ITERATIONS) .timeUnit(TimeUnit.MILLISECONDS) .forks(0 ) .threads(8 ) .mode(Mode.AverageTime) .shouldDoGC(true ) .shouldFailOnError(true ) .resultFormat(ResultFormatType.JSON) .result("Main.json" ) .shouldFailOnError(true ) .jvmArgs("-server" ) .build(); new Runner (opt).run(); } public static class VolatileLong { private volatile long value = 0 ; public synchronized void add (long amount) { this .value += amount; } public long getValue () { return this .value; } } @Benchmark public void atomicLong (Blackhole blackhole) { AtomicLong atomicLong = new AtomicLong (); numberList.parallelStream().forEach(atomicLong::addAndGet); blackhole.consume(atomicLong.get()); } @Benchmark public void volatileLong (Blackhole blackHole) { VolatileLong volatileLong = new VolatileLong (); numberList.parallelStream().forEach(volatileLong::add); blackHole.consume(volatileLong.getValue()); } @Benchmark public void longStreamSum (Blackhole blackHole) { long sum = numberList.parallelStream().mapToLong(s -> s).sum(); blackHole.consume(sum); } }
启动 1 2 ➜ JMH git:(main) ✗ cd target ➜ target git:(main) ✗ java -jar jmh.jar
END