JMH即Java Microbenchmark Harness.是由开发JVM的那些大佬开发出的Micro Benchmark Framework.理论上支持各种在JVM上运行的语言。
何为Micro Benchmark Framework?简单的说就是在方法级层面的benchmarck。通常来说,随着系统整体复杂性的不断提升,精准的衡量系统的一个单元的性能愈加困难(如单独进行测试系统的某个部分时,因为JVM或硬件在该场景下可能会产生了一些执行优化,而这些优化在生产环境中是无法应用的,这将导致这类的独立测试不够准确)。
此外借助于JMH框架也能较快速简洁的完成独立的模块测试,有助于在开发高性能应用程序时对性能进行优化。
比较经典的使用场景有:
当然使用场景也不仅仅局限于此。
首先引入依赖
1.21
org.openjdk.jmh
jmh-core
${jmh.version}
org.openjdk.jmh
jmh-generator-annprocess
${jmh.version}
provided
如现在想测试一个方法的耗时分布
@BenchmarkMode(Mode.SampleTime) // 该模式可以测试耗时分布情况
@OutputTimeUnit(TimeUnit.MILLISECONDS) // 输出的时间单位
@State(Scope.Benchmark)
public class JmhTest2 {
@Benchmark
public void test() throws InterruptedException {
TimeUnit.MILLISECONDS.sleep(10);
}
public static void main(String[] args) throws RunnerException {
// 可以通过注解来进行配置
Options opt = new OptionsBuilder()
.include(JmhTest2.class.getSimpleName())
.warmupIterations(3) // 预热3次
.measurementIterations(2).measurementTime(TimeValue.valueOf("5s")) // 迭代2次,每次5秒
.threads(10) // 10线程并发
.forks(1)
.build();
new Runner(opt).run();
}
}
输出
# 以下是配置信息
# JMH version: 1.21
# VM version: JDK 1.8.0_171, Java HotSpot(TM) 64-Bit Server VM, 25.171-b11
# VM invoker: D:\Program\java1.8\jdk1.8\jre\bin\java.exe
# VM options: -Dvisualvm.id=24535188607243 -javaagent:D:\Program\idea\lib\idea_rt.jar=49279:D:\Program\idea\bin -Dfile.encoding=UTF-8
# Warmup: 3 iterations, 10 s each
# Measurement: 2 iterations, 5 s each
# Timeout: 10 min per iteration
# Threads: 10 threads, will synchronize iterations
# Benchmark mode: Sampling time
# Benchmark: top.amazingwu.jmh.JmhTest2.test
# 迭代信息
# Run progress: 0.00% complete, ETA 00:00:40
# Fork: 1 of 1
# Warmup Iteration 1: SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
10.007 ±(99.9%) 0.013 ms/op
# Warmup Iteration 2: 9.990 ±(99.9%) 0.001 ms/op
# Warmup Iteration 3: 9.989 ±(99.9%) 0.002 ms/op
Iteration 1: 9.989 ±(99.9%) 0.003 ms/op
test·p0.00: 9.535 ms/op
test·p0.50: 9.994 ms/op
test·p0.90: 10.060 ms/op
test·p0.95: 10.109 ms/op
test·p0.99: 10.158 ms/op
test·p0.999: 10.256 ms/op
test·p0.9999: 10.306 ms/op
test·p1.00: 10.306 ms/op
Iteration 2: 9.994 ±(99.9%) 0.006 ms/op
test·p0.00: 9.093 ms/op
test·p0.50: 9.994 ms/op
test·p0.90: 10.109 ms/op
test·p0.95: 10.142 ms/op
test·p0.99: 10.207 ms/op
test·p0.999: 12.239 ms/op
test·p0.9999: 12.960 ms/op
test·p1.00: 12.960 ms/op
# 结果
Result "top.amazingwu.jmh.JmhTest2.test":
N = 10002
mean = 9.991 ±(99.9%) 0.003 ms/op
Histogram, ms/op:
[ 9.000, 9.250) = 2
[ 9.250, 9.500) = 2
[ 9.500, 9.750) = 29
[ 9.750, 10.000) = 6290
[10.000, 10.250) = 3656
[10.250, 10.500) = 11
[10.500, 10.750) = 2
[10.750, 11.000) = 3
[11.000, 11.250) = 0
[11.250, 11.500) = 0
[11.500, 11.750) = 0
[11.750, 12.000) = 0
[12.000, 12.250) = 3
[12.250, 12.500) = 2
[12.500, 12.750) = 1
Percentiles, ms/op:
p(0.0000) = 9.093 ms/op
p(50.0000) = 9.994 ms/op
p(90.0000) = 10.093 ms/op
p(95.0000) = 10.125 ms/op
p(99.0000) = 10.174 ms/op
p(99.9000) = 10.796 ms/op
p(99.9900) = 12.960 ms/op
p(99.9990) = 12.960 ms/op
p(99.9999) = 12.960 ms/op
p(100.0000) = 12.960 ms/op
# Run complete. Total time: 00:00:41
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
experiments, perform baseline and negative tests that provide experimental control, make sure
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
Do not assume the numbers tell you what you want them to tell.
Benchmark Mode Cnt Score Error Units
JmhTest2.test sample 10002 9.991 ± 0.003 ms/op
JmhTest2.test:test·p0.00 sample 9.093 ms/op
JmhTest2.test:test·p0.50 sample 9.994 ms/op
JmhTest2.test:test·p0.90 sample 10.093 ms/op // 90% 每次执行耗时10.093ms以内,以下类推
JmhTest2.test:test·p0.95 sample 10.125 ms/op
JmhTest2.test:test·p0.99 sample 10.174 ms/op
JmhTest2.test:test·p0.999 sample 10.796 ms/op
JmhTest2.test:test·p0.9999 sample 12.960 ms/op
JmhTest2.test:test·p1.00 sample 12.960 ms/op
Process finished with exit code 0
共有四种模式:
Warmup 是指在实际进行 benchmark前先进行预热的行为。为什么需要预热?因为JVM的JIT机制的存在,如果某个函数被调用多次之后,JVM会尝试将其编译成为机器码从而提高执行速度。所以为了让 benchmark 的结果更加接近真实情况就需要进行预热。
与JUnit的@Test类似,即一个测试单元
State 用于声明某个类是一个“状态”,然后接受一个Scope参数用来表示该状态的共享范围。因为很多benchmark会需要一些表示状态的类,JMH允许你把这些类以依赖注入的方式注入到benchmark函数里。Scope主要分为两种。
例子:
// 所有线程共享
@State(Scope.Benchmark)
public static class BenchmarkState {
volatile double x = Math.PI;
}
// 线程独享
@State(Scope.Thread)
public static class ThreadState {
volatile double x = Math.PI;
}
// ThreadState state将会通过依赖注入的形式注入
@Benchmark
public void measureUnshared(ThreadState state) {
state.x++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("measureUnshared:"+ state.x);
}
@Benchmark
public void measureShared(BenchmarkState state) {
state.x++;
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("measureShared:"+ state.x);
}
benchmark 结果所使用的时间单位。
@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。
@Setup 会在执行 benchmark 之前被执行,正如其名,主要用于初始化。
@TearDown 和 @Setup 相对的,会在所有 benchmark 执行结束以后执行,主要用于资源的回收等。
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class SecondBenchmark {
@Param({"10000", "100000", "1000000"})
private int length;
private int[] numbers;
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(SecondBenchmark.class.getSimpleName())
.forks(2)
.warmupIterations(5)
.measurementIterations(5)
.build();
new Runner(opt).run();
}
@Benchmark
public void singleThreadBench() {
}
@Benchmark
public void multiThreadBench() {
return multiThreadCalc.sum(numbers);
}
@Setup
public void prepare() {
// 初始化,可以使用参数length
numbers = IntStream.rangeClosed(1, length).toArray();
}
@TearDown
public void shutdown() {
// 资源回收
}
}
如上文使用的启动项配置(以下配置只是常用的部分,同时这些配置可以通过注解来实现):
// 可以通过注解来进行配置
Options opt = new OptionsBuilder()
.include(JmhTest2.class.getSimpleName())
.warmupIterations(3) // 预热3次
.measurementIterations(2).measurementTime(TimeValue.valueOf("5s")) // 迭代2次,每次5秒
.threads(10) // 10线程并发
.forks(1)
.build();
new Runner(opt).run();
benchmark 所在的类的名字,注意这里是使用正则表达式对所有类进行匹配的。
预热的迭代次数。
实际测量的迭代次数。
并发线程数
进行 fork 的次数。如果fork数是2的话,则JMH会fork出两个进程来进行测试。主要用于减少运行结果的差异化
在Intellij Idea中可以搜索插件JMH。