网易首页 > 网易号 > 正文 申请入驻

顶级Java才懂的,基准测试JMH!

0
分享至

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

最近在手写一个ID生成器,需要比较UUID和目前比较流行的NanoID之间的速度差异,当然也要测一下根据规则自创的ID生成器。

这样的代码属于最基础的API,速度哪怕减上几纳秒,累加起来也是很可观的。关键是,我该如何评估ID的生成速度呢?

1. 如何统计性能?

常见的方法,是写一些统计代码。这些代码,穿插在我们的逻辑中,进行一些简单的计时运算。比如下面这几行:

long start = System.currentTimeMillis();
//logic
long cost = System.currentTimeMillis() - start;
System.out.println("Logic cost : " + cost);

这样的统计方式,用在业务代码里,哪怕是APM里,并不见得有什么问题。

可惜的是,这段代码的统计结果,并不见得一定准确。举个例子来说,JVM在执行时,会对一些代码块,或者一些频繁执行的逻辑,进行JIT编译和内联优化,在得到一个稳定的测试结果之前,需要先循环上上万次,进行预热。预热前和预热后的性能差别是非常大的。

另外,评估性能,有很多的指标。如果这些指标数据,每次都要手工去算的话,那肯定是枯燥乏味且低效的。

JMH(the Java Microbenchmark Harness) 就是这样一个能够做基准测试的工具。如果你通过我们一系列的工具,定位到了热点代码,要测试它的性能数据,评估改善情况,就可以交给JMH。它的测量精度非常高,最高可达到纳秒的级别。

JMH已经在JDK 12中被包含,其他版本的需要自行引入maven,坐标如下。


org.openjdk.jmhgroupId>
jmh-coreartifactId>
1.23version>
dependency>
org.openjdk.jmhgroupId>
jmh-generator-annprocessartifactId>
1.23version>
providedscope>
dependency>
dependencies>

下面,我们介绍一下这个工具的使用。

2. 关键注解

JMH是一个jar包,它和单元测试框架JUnit非常的像,可以通过注解进行一些基础配置。这部分配置有很多是可以通过main方法的OptionsBuilder进行设置的。

上图是一个典型的JMH程序执行的内容。通过开启多个进程,多个线程,首先执行预热,然后执行迭代,最后汇总所有的测试数据进行分析。在执行前后,还可以根据粒度处理一些前置和后置操作。

一个简单的代码如下:

@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@Threads(2)
public class BenchmarkTest {
@Benchmark
public long shift() {
long t = 455565655225562L;
long a = 0;
for (int i = 0; i < 1000; i++) {
a = t >> 30;
return a;

@Benchmark
public long div() {
long t = 455565655225562L;
long a = 0;
for (int i = 0; i < 1000; i++) {
a = t / 1024 / 1024 / 1024;
}
return a;
}

public static void main(String[] args) throws Exception {
Options opts = new OptionsBuilder()
.include(BenchmarkTest.class.getSimpleName())
.resultFormat(ResultFormatType.JSON)
.build();
new Runner(opts).run();
}
}

下面,我们逐一介绍一下比较关键的注解和参数。

@Warmup

样例。

@Warmup(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)

我们不止一次提到预热,warmup这个注解,可以用在类或者方法上,进行预热配置。可以看到,它有几个配置参数。

  • timeUnit:时间的单位,默认的单位是秒。

  • iterations:预热阶段的迭代数。

  • time:每次预热的时间。

  • batchSize:批处理大小,指定了每次操作调用几次方法。

上面的注解,意思是对代码预热总计5秒(迭代5次,每次一秒) 。预热过程的测试数据,是不记录测量结果的。

我们可以看一下它执行的效果:

# Warmup: 3 iterations, 1 s each
# Warmup Iteration 1: 0.281 ops/ns
# Warmup Iteration 2: 0.376 ops/ns
# Warmup Iteration 3: 0.483 ops/ns

一般来说,基准测试都是针对的比较小的、执行速度相对较快的代码块。这些代码有很大的可能被编译、内联,在编码的时候保持方法的精简,对JIT也是有好的。

说到预热,就不得不提一下在分布式环境下的服务预热。在对服务节点进行发布的时候,通常也会有预热过程,逐步放量到相应的服务节点,直到服务达到最优状态。如下图所示,负载均衡负责这个放量过程,一般是根据百分比进行放量。

@Measurement

样例如下。

@Measurement(
iterations = 5,
time = 1,
timeUnit = TimeUnit.SECONDS)

MeasurementWarmup的参数是一样的。不同于预热,它指的是真正的迭代次数。

我们能够从日志中看到这个执行过程:

# Measurement: 5 iterations, 1 s each
Iteration 1: 1646.000 ns/op
Iteration 2: 1243.000 ns/op
Iteration 3: 1273.000 ns/op
Iteration 4: 1395.000 ns/op
Iteration 5: 1423.000 ns/op

虽然经过预热之后,代码都能表现出它的最优状态,但一般和实际应用场景还是有些出入的。如果你的测试机器性能很高,或者你的测试机资源利用已经达到了极限,都会影响测试结果的数值。通常情况下,我都会在测试的时候,给机器充足的资源,保持一个稳定的环境。在分析结果的时候,也更加关注不同实现方式的性能差异,而不是测试数据本身。

@BenchmarkMode

此注解用来指定基准测试类型,对应Mode选项,用来修饰类和方法都可以。这里的value,是一个数组,可以配置多个统计维度。比如:

@BenchmarkMode({Throughput,Mode.AverageTime})。统计的就是吞吐量和平均执行时间两个指标。

所谓的模式,在JMH中,可以分为以下几种:

  • Throughput:整体吞吐量,比如QPS,单位时间内的调用量等。

  • AverageTime:平均耗时,指的是每次执行的平均时间。如果这个值很小不好辨认,可以把统计的单位时间调小一点。

  • SampleTime:随机取样

  • SingleShotTime:如果你想要测试仅仅一次的性能,比如第一次初始化花了多长时间,就可以使用这个参数,其实和传统的main方法没有什么区别。

  • All:所有的指标,都算一遍,你可以设置成这个参数看下效果。

我们拿平均时间,看一下一个大体的执行结果:

Result "com.github.xjjdog.tuning.BenchmarkTest.shift":
2.068 ±(99.9%) 0.038 ns/op [Average]
(min, avg, max) = (2.059, 2.068, 2.083), stdev = 0.010
CI (99.9%): [2.030, 2.106] (assumes normal distribution)

由于我们声明的时间单位是纳秒,本次shift方法的平均响应时间就是2.068纳秒。

我们也可以看下最终的耗时时间。

Benchmark Mode Cnt Score Error Units
BenchmarkTest.div avgt 5 2.072 ± 0.053 ns/op
BenchmarkTest.shift avgt 5 2.068 ± 0.038 ns/op

由于是平均数,这里的Error值的是误差的意思(或者波动)。

可以看到,在衡量这些指标的时候,都有一个时间维度,它就是通过**@OutputTimeUnit**注解进行配置的。

这个就比较简单了,它指明了基准测试结果的时间类型。可用于类或者方法上。一般选择秒、毫秒、微秒,纳秒那是针对的速度非常快的方法。

举个例子,@BenchmarkMode(Mode.Throughput)@OutputTimeUnit(TimeUnit.MILLISECONDS)进行组合,代表的就是每毫秒的吞吐量。

如下面的关于吞吐量的结果,就是以毫秒计算的。

Benchmark Mode Cnt Score Error Units
BenchmarkTest.div thrpt 5 482999.685 ± 6415.832 ops/ms
BenchmarkTest.shift thrpt 5 480599.263 ± 20752.609 ops/ms

OutputTimeUnit注解同样可以修饰类或者方法,通过更改时间级别,可以获取更加易读的结果。

@Fork

fork的值一般设置成1,表示只使用一个进程进行测试;如果这个数字大于1,表示会启用新的进程进行测试;但如果设置成0,程序依然会运行,不过这样是在用户的JVM进程上运行的,可以看下下面的提示,但不推荐这么做。

# Fork: N/A, test runs in the host VM
# *** WARNING: Non-forked runs may silently omit JVM options, mess up profilers, disable compiler hints, etc. ***
# *** WARNING: Use non-forked runs only for debugging purposes, not for actual performance runs. ***

那么fork到底是在进程还是线程环境里运行呢?我们追踪一下JMH的源码,发现每个fork进程是单独运行在Proccess进程里的,这样就可以做完全的环境隔离,避免交叉影响。它的输入输出流,通过Socket连接的模式,发送到我们的执行终端。

在这里分享一个小技巧。其实fork注解有一个参数叫做jvmArgsAppend,我们可以通过它传递一些JVM的参数。

@Fork(value = 3, jvmArgsAppend = {"-Xmx2048m", "-server", "-XX:+AggressiveOpts"})

在平常的测试中,也可以适当增加fork数,来减少测试的误差。

@Threads

fork是面向进程的,而Threads是面向线程的。指定了这个注解以后,将会开启并行测试。

如果配置了 Threads.MAX ,则使用和处理机器核数相同的线程数。

@Group

@Group注解只能加在方法上,用来把测试方法进行归类。如果你单个测试文件中方法比较多,或者需要将其归类,则可以使用这个注解。

与之关联的@GroupThreads注解,会在这个归类的基础上,再进行一些线程方面的设置。

@State

@State 指定了在类中变量的作用范围。它有三个取值。

@State 用于声明某个类是一个“状态”,可以用Scope 参数用来表示该状态的共享范围。这个注解必须加在类上,否则提示无法运行。

Scope有如下3种值:

  • Benchmark:表示变量的作用范围是某个基准测试类。

  • Thread:每个线程一份副本,如果配置了Threads注解,则每个Thread都拥有一份变量,它们互不影响。

  • Group:联系上面的@Group注解,在同一个Group里,将会共享同一个变量实例。

JMHSample04DefaultState测试文件中,演示了变量x的默认作用范围是Thread,关键代码如下:

@State(Scope.Thread)
public class JMHSample_04_DefaultState {
double x = Math.PI;
@Benchmark
public void measure() {
x++;

@Setup和@TearDown

和单元测试框架JUnit类似,用于基准测试前的初始化动作, @TearDown 用于基准测试后的动作,来做一些全局的配置。

这两个注解,同样有一个Level值,标明了方法运行的时机,它有三个取值。

  • Trial:默认的级别。也就是Benchmark级别。

  • Iteration:每次迭代都会运行。

  • Invocation:每次方法调用都会运行,这个是粒度最细的。

@Param

@Param 注解只能修饰字段,用来测试不同的参数,对程序性能的影响。配合@State注解,可以同时制定这些参数的执行范围。

代码样例如下:

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
public class JMHSample_27_Params {
@Param({"1", "31", "65", "101", "103"})
public int arg;
@Param({"0", "1", "2", "4", "8", "16", "32"})
public int certainty;
@Benchmark
public boolean bench() {
return BigInteger.valueOf(arg).isProbablePrime(certainty);
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(JMHSample_27_Params.class.getSimpleName())
// .param("arg", "41", "42") // Use this to selectively constrain/override parameters
.build();

new Runner(opt).run();
}
}

值得注意的是,如果你设置了非常多的参数,这些参数将执行多次,通常会运行很长时间。比如参数1 M个,参数2 N个,那么总共要执行M*N次。

下面是一个执行结果的截图。

@CompilerControl

这可以说是一个非常有用的功能了。

Java中方法调用的开销是比较大的,尤其是在调用量非常大的情况下。拿简单的getter/setter方法来说,这种方法在Java代码中大量存在。我们在访问的时候,就需要创建相应的栈帧,访问到需要的字段后,再弹出栈帧,恢复原程序的执行。

如果能够把这些对象的访问和操作,纳入到目标方法的调用范围之内,就少了一次方法调用,速度就能得到提升,这就是方法内联的概念。如图所示,代码经过JIT编译之后,效率会有大的提升。

这个注解可以用在类或者方法上,能够控制方法的编译行为,常用的有3种模式。

强制使用内联(INLINE),禁止使用内联(DONT_INLINE),甚至是禁止方法编译(EXCLUDE)等。

2.将结果图形化

使用JMH测试的结果,可以二次加工,进行图形化展示。结合图表数据,更加直观。通过运行时,指定输出的格式文件,即可获得相应格式的性能测试结果。

比如下面这行代码,就是指定输出JSON格式的数据。

Options opt = new OptionsBuilder()
.resultFormat(ResultFormatType.JSON)
.build();

JMH支持以下5种格式的结果:

  • TEXT导出文本文件。

  • CSV导出csv格式文件。

  • SCSV导出scsv等格式的文件。

  • JSON导出成json文件。

  • LATEX导出到latex,一种基于ΤΕΧ的排版系统。

一般来说,我们导出成CSV文件,直接在Excel中操作,生成相应的图形就可以了。

另外介绍几个可以做图的工具:

JMH Visualizer这里有一个开源的项目(https://jmh.morethan.io/) ,通过导出json文件,上传之后,可得到简单的统计结果。个人认为它的展示方式并不是很好。

jmh-visual-chart

相比较而言,下面这个工具(http://deepoove.com/jmh-visual-chart) ,就相对直观一些。

meta-chart

一个通用的在线图表生成器。(https://www.meta-chart.com/),导出CSV文件后,做适当处理,即可导出精美图像。

像Jenkins等一些持续集成工具,也提供了相应的插件,用来直接显示这些测试结果。

END

这个工具非常好用,它使用确切的测试数据,来支持我们的分析结果。一般情况下,如果定位到热点代码,就需要使用基准测试工具进行专项优化,直到性能有了显著的提升。

在我们的这个场景中,就发现使用NanoID,确实是比UUID要快上好多。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

3.
4.
5.
6.

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
山东一民企飞无人机催县农业局“快还钱”,当地回应:正协商解决

山东一民企飞无人机催县农业局“快还钱”,当地回应:正协商解决

上游新闻
2026-05-26 19:50:19
白衣飘飘,绿树依依!

白衣飘飘,绿树依依!

疾跑的小蜗牛
2026-05-27 22:49:26
一顿饭就要花掉40万,四年敛财40亿,杭州土皇帝虞关荣有多嚣张

一顿饭就要花掉40万,四年敛财40亿,杭州土皇帝虞关荣有多嚣张

莫地方
2026-05-21 01:45:03
穆里尼奥太离谱!门德斯送上 1.6 亿双星!两人点名要为他效力

穆里尼奥太离谱!门德斯送上 1.6 亿双星!两人点名要为他效力

奶盖熊本熊
2026-05-28 04:29:05
承诺“不限次数”,就该接住顾客的“天天来”

承诺“不限次数”,就该接住顾客的“天天来”

南风不及你温柔
2026-05-16 01:02:57
脸都打肿了!曼城 8000 万目标直接拒绝!亲口表态首选曼联

脸都打肿了!曼城 8000 万目标直接拒绝!亲口表态首选曼联

奶盖熊本熊
2026-05-28 05:24:01
新加坡为何害怕《给阿嬷的情书》?跟多年来他们刻意去中国化有关

新加坡为何害怕《给阿嬷的情书》?跟多年来他们刻意去中国化有关

娱乐圈见解说
2026-05-27 00:48:02
卫健委已将左氧氟沙星列为重点监控药物!提醒:服用千万注意

卫健委已将左氧氟沙星列为重点监控药物!提醒:服用千万注意

健康科普365
2026-05-25 22:15:03
47.98 万元起!新一代问界 M9 上市,余承东:地球上最强的 SUV

47.98 万元起!新一代问界 M9 上市,余承东:地球上最强的 SUV

爱范儿
2026-05-28 00:25:31
多名院士调查发现:吃一口久冻馒头,等于中一次毒?真假

多名院士调查发现:吃一口久冻馒头,等于中一次毒?真假

医学科普汇
2026-05-27 20:00:13
知名户外主播发生意外,Z疮发作大便失禁,恶臭难挡被取笑

知名户外主播发生意外,Z疮发作大便失禁,恶臭难挡被取笑

新游戏大妹子
2026-05-27 12:54:49
你在无意中发现别人什么秘密?网友爆料,电视剧都不敢这样演

你在无意中发现别人什么秘密?网友爆料,电视剧都不敢这样演

夜深爱杂谈
2026-03-16 22:21:03
1990年,作家三毛到新疆和76岁的王洛宾同居,王洛宾说:“可以同居,不可以发生关系!

1990年,作家三毛到新疆和76岁的王洛宾同居,王洛宾说:“可以同居,不可以发生关系!

犀利辣椒
2026-05-20 06:23:07
体育世家,日本U21国脚萨尼-布朗的哥哥是日本百米运动员

体育世家,日本U21国脚萨尼-布朗的哥哥是日本百米运动员

懂球帝
2026-05-27 15:55:09
老赖黄淑芬把人撞成植物人,宁愿坐牢不肯赔偿,如今结果大快人心

老赖黄淑芬把人撞成植物人,宁愿坐牢不肯赔偿,如今结果大快人心

米果说识
2026-04-03 14:56:49
无名指长于食指为龙长虎短,这类手相之人晚年有三种不同运势

无名指长于食指为龙长虎短,这类手相之人晚年有三种不同运势

唠叨说历史
2026-05-27 14:59:27
老婆孩子在哈尔滨,已婚男子孤身在北京,两年打赏女主播1700多万元,七夕等节庆日还飞去上海和女方约会!妻子起诉三被告,法院判了

老婆孩子在哈尔滨,已婚男子孤身在北京,两年打赏女主播1700多万元,七夕等节庆日还飞去上海和女方约会!妻子起诉三被告,法院判了

扬子晚报
2026-05-27 20:12:23
四亿人同吃一锅饭:大跃进公共食堂是如何把好事办成噩梦的?

四亿人同吃一锅饭:大跃进公共食堂是如何把好事办成噩梦的?

浪子说
2026-05-28 00:50:03
史诗级突破!随着水晶宫夺欧协联冠军,英超近半数球队获欧战席位

史诗级突破!随着水晶宫夺欧协联冠军,英超近半数球队获欧战席位

侧身凌空斩
2026-05-28 06:52:48
14岁小七穿贝嫂的挂脖裙逛街,戴牙套笑容灿烂,进军美妆业受阻

14岁小七穿贝嫂的挂脖裙逛街,戴牙套笑容灿烂,进军美妆业受阻

译言
2026-05-27 09:28:14
2026-05-28 09:12:49
小姐姐味道
小姐姐味道
十年架构,日百亿流量
329文章数 1203关注度
往期回顾 全部

科技要闻

拼多多股价跌10%:管理层称业绩难免波动

头条要闻

台媒:特朗普就台湾问题表态后 没人敢提“台独”了

头条要闻

台媒:特朗普就台湾问题表态后 没人敢提“台独”了

体育要闻

这群老阿姨,是最硬核的马刺球迷

娱乐要闻

王鹤棣风波连累父亲炸串店遭差评?

财经要闻

一线调查丨燃油车“甩卖”也难卖

汽车要闻

限时补贴价9.28-10.98万 MG 4X正式上市

态度原创

亲子
数码
教育
家居
公开课

亲子要闻

专家:别在小事上消耗孩子!网友:瞎扯!硬控自己情绪,满身结节

数码要闻

罗技K98M PLUS机械键盘新增“冰淇淋小熊”版本,569元

教育要闻

下周天就高考了,刷到的一定考的全会蒙的全对!

家居要闻

古老而持久 石影扶手椅

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版