JMH: 最装逼,最牛逼的基准测试工具套件

JMH简介

官网:http://openjdk.java.net/projects/code-tools/jmh/

简介:JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targetting the JVM,由简介可知,JMH不止能对Java语言做基准测试,还能对运行在JVM上的其他语言做基准测试。而且可以分析到纳秒级别。

推荐用法

官方推荐创建一个独立的Maven工程来运行JMH基准测试,这样更能确保结果的准确性。当然也可以在已存在的工程中,或者在IDE上运行,但是越复杂,结果越不可靠(more complex and the results are less reliable)。

简单实用

推荐用法通过命令行创建,构建和运行JMH基准测试。

setup

生成一个新的JMH工程的maven命令如下:

 mvn archetype:generate 
 -DinteractiveMode=false 
 -DarchetypeGroupId=org.openjdk.jmh 
 -DarchetypeArtifactId=jmh-java-benchmark-archetype 
 -DgroupId=com.afei.jmh 
 -DartifactId=jmh 
 -Dversion=1.0.0-SNAPSHOT

执行该命令后,会创建一个Maven工程,但是默认生成的MyBenchmark.java并没有在预期的包名com/afei/jmh中,即使加上参数-DpackageName=com.afei.jmh也不行,只能先手动将其挪到包名下,这里作为一个小小的遗留问题。

压测代码

默认生成的MyBenchmark.java源码如下,testMethod()中就是你要压测的代码,下面是笔者要压测的洗牌算法:

@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {

    @GenerateMicroBenchmark
    public List<Integer> testMethod() {
        int cardCount = 54;
        List<Integer> cardList = new ArrayList<Integer>();
        for (int i=0; i<cardCount; i++){
            cardList.add(i);
        }
        // 洗牌算法
        Random random = new Random();
        for (int i=0; i<cardCount; i++) {
            int rand = random.nextInt(cardCount);
            Collections.swap(cardList, i, rand);
        }
        return cardList;
    }

}

build

写完代码接下来就是构建并打包,在pom.xml所在目录执行如下命令:

mvn clean package

说明:这一步,也可以通过IDE工具构建打包。

running

打包成功后在target目录下生成了一个JAR文件:microbenchmarks.jar,需要注意的是,官网的运行命令是java -jar target/benchmarks.jar,至于到底是benchmarks.jar还是microbenchmarks.jar,取决于你的POM文件:

<configuration>
    <finalName>microbenchmarks</finalName>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>org.openjdk.jmh.Main</mainClass>
        </transformer>
    </transformers>
</configuration>

笔者生成的maven工程是microbenchmarks,所以,运行时执行如下命令:

java -jar target/microbenchmarks.jar

输出结果如下:

# Run progress: 0.00% complete, ETA 00:00:10
# VM invoker: C:\Program Files\Java\jre1.8.0_181\bin\java.exe
# VM options: <none>
# Fork: 1 of 1
# Warmup: 5 iterations, 1 s each
# Measurement: 5 iterations, 1 s each
# Threads: 1 thread, will synchronize iterations
# Benchmark mode: Average time, time/op
# Benchmark: com.afei.jmh.MyBenchmark.testMethod
# Warmup Iteration   1: 1133.738 ns/op
# Warmup Iteration   2: 1169.750 ns/op
# Warmup Iteration   3: 1066.204 ns/op
# Warmup Iteration   4: 1086.300 ns/op
# Warmup Iteration   5: 1145.228 ns/op
Iteration   1: 1045.157 ns/op
Iteration   2: 1064.303 ns/op
Iteration   3: 1064.227 ns/op
Iteration   4: 1053.979 ns/op
Iteration   5: 1055.718 ns/op

Result : 1056.677  ±(99.9%) 30.809 ns/op
  Statistics: (min, avg, max) = (1045.157, 1056.677, 1064.303), stdev = 8.001
  Confidence interval (99.9%): [1025.868, 1087.486]

Benchmark                        Mode   Samples         Mean   Mean error    Units
c.a.j.MyBenchmark.testMethod     avgt         5     1056.677       30.809    ns/op

结果解读

下面对输出结果一些重要信息进行解读:

@Warmup

由于笔者加了这个注解:@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)。所以,基准测试后对代码预热总计5秒(迭代5次,每次1秒)。预热对于压测来说非常非常重要,如果没有预热过程,压测结果会很不准确。这个注解对应的日志如下:

# Warmup Iteration   1: 1133.738 ns/op
# Warmup Iteration   2: 1169.750 ns/op
# Warmup Iteration   3: 1066.204 ns/op
# Warmup Iteration   4: 1086.300 ns/op
# Warmup Iteration   5: 1145.228 ns/op

@Measurement

另外一个重要的注解:@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS),表示循环运行5次,总计5秒时间。

@Fork

这个注解表示fork多少个线程运行基准测试,如果@Fork(1),那么就是一个线程,这时候就是同步模式。

@BenchmarkMode&@OutputTimeUnit

基准测试模式申明为:@BenchmarkMode(Mode.AverageTime)搭配@OutputTimeUnit(TimeUnit.NANOSECONDS)(可选基准测试模式通过枚举Mode得到),笔者的示例是AverageTime,即表示每次操作需要的平均时间,而OutputTimeUnit申明为纳秒,所以基准测试单位是ns/op,即每次操作的纳秒单位平均时间。基准测试结果如下:

Result : 1056.677 ±(99.9%) 30.809 ns/op
  Statistics: (min, avg, max) = (1045.157, 1056.677, 1064.303), stdev = 8.001
  Confidence interval (99.9%): [1025.868, 1087.486]

最后一段结果如下,重点关注MeanUnits两个字段,组合起来就是1227.928ns/op,即每次操作耗时1056.677纳秒:

Benchmark                        Mode   Samples         Mean   Mean error    Units
c.a.j.MyBenchmark.testMethod     avgt         5     1056.677       30.809    ns/op

如果我们将@BenchmarkMode(Mode.AverageTime)@OutputTimeUnit(TimeUnit.NANOSECONDS)的组合,改成@BenchmarkMode(Mode.Throughput)@OutputTimeUnit(TimeUnit.MILLISECONDS),那么基准测试结果就是每毫秒的吞吐量(即每毫秒多少次操作),结果如下,表示943.437ops/ms:

Benchmark                        Mode   Samples         Mean   Mean error    Units
c.a.j.MyBenchmark.testMethod    thrpt         5      943.437       44.060   ops/ms

Mean error表示误差,或者波动,与Result的±值对应:Result : 1056.677 ±(99.9%) 30.809 ns/op

基准测试对比

将自定义洗牌算法和JDK原生的洗牌算法Collections.shuffle(cardList);进行基准测试对比,结果如下:

- ops/ms ns/op
JDK原生洗牌算法 807.470 1149.900
自定义洗牌算法(for循环外面new Random) 943.437 1056.677
自定义洗牌算法(for循环里面new Random) 300.467 3346.509

Random在for循环里面的源码如下:

for (int i=0; i<cardCount; i++) {
    Random random = new Random();
    int rand = random.nextInt(cardCount);
    Collections.swap(cardList, i, rand);
}

说明,自定义洗牌算法事实上就是JDK自带洗牌算法中集合的size少于SHUFFLE_THRESHOLD(这个值为5)时的实现。另外,由基准测试对比可知,for循环里面不断new Random的性能相比只在for循环外面new Random一次的性能要差好3倍左右。

另外,在集合的size超过SHUFFLE_THRESHOLD即5后JDK原生洗牌算法,相比size少于该值得洗牌算法性能并没有提高,两者性能差在10%左右,基本可以忽略。这里想不明白,JDK原生洗牌算法在集合的size超过SHUFFLE_THRESHOLD的优化的意思,当然也可能跟笔者基准测试样本有关(毕竟笔者只测试了size为54的集合)。

JMH和jMeter的不同

JMH和jMeter的使用场景还是有很大的不同的,jMeter更多的是对rest api进行压测,而JMH关注的粒度更细,它更多的是发现某块性能槽点代码,然后对优化方案进行基准测试对比。比如json序列化方案对比,bean copy方案对比,文中提高的洗牌算法对比等。

案例参考

官方给了很多样例代码,有兴趣的同学可以自己查询并学习JMH:http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,594评论 18 139
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,424评论 25 707
  • 1.炒股 不能追涨杀跌,一追涨就失去先手,买入成本价就比较高,没有成本优势。 一般来说追高买入,买入价会比较高,而...
    shanshukeng阅读 353评论 0 0
  • 困啦,睡吧,又玩一晚上,花了28 大餐额 最得意的说 永红要走了,他们都要支教啦, 对了我居然问报教师资格证的人要...
    宋长金j阅读 77评论 0 0
  • 有些人活成了自己生命的旁观者。 成长的路上有一条弯路,叫活成了自己生命的旁观者。 学习相同的心理学课程,有人飞快的...
    梓茗森林阅读 270评论 0 0