- jmh - samples | GitHub
- Avoiding Benchmarking Pitfalls on the JVM
- Is Protobuf 5x Faster Than JSON? (Part 2)
概念与基本使用
Iteration
:JMH 进行测试的最小单位,包含一组 invocationInvocation
:一次 benchmark 方法调用
使用 @Benchmark
修饰要测试的方法,就像 JUnit 的 @Test
一样。@BenchmarkMode(Mode[])
指定从哪些维度做测量和统计。
org.openjdk.jmh.annotations.Mode |
含义 |
---|---|
Mode.Throughput |
吞吐量,单位时间内能够执行多少次调用(ops/time ) |
Mode.AverageTime |
调用方法的平均耗时(time/op ) |
Mode.SampleTime |
持续执行一段时间,按特定频率采样获取方法的执行耗时 |
Mode.SingleShotTime |
仅执行一次,通常用于评估冷启动性能 |
Mode.All |
当 JVM 发现某个方法或代码块运行时执行的特别频繁的时候,就会认为这是 Hot Spot Code(热点代码)。JIT 会对热点代码进行优化,因此实际测试前需要预热。使用 Warmup
注解声明预热方式,常用以下参数:
time
:每次预热持续的时间timeUnit
:时间单位,默认是秒iterations
:预热阶段的迭代数
预热阶段不会测量数据,使用 Measurement
描述真正测量阶段的配置,参数及含义与 Warmup
相同。
待测方法需要反复执行多次,但是有一些承载状态的对象是固定不变的。使用 State
注解标记这些对象,其 Scope
属性表示作用范围:
org.openjdk.jmh.annotations.Scope |
作用范围 |
---|---|
Scope.Benchmark |
变量的作用范围是某个基准测试类 |
Scope.Group |
同一个 Group 里的测试方法共享一个变量实例 |
Scope.Thread |
每个 Thread 拥有独立副本 |
@Setup
和 @TearDown
修饰的方法分别在测试方法运行前后执行,可以用于资源的初始化与释放。这两个注解的 Level
属性标明了方法运行的时机。
org.openjdk.jmh.annotations.Level |
含义 |
---|---|
Level.Trial |
默认,Benchmark 级别 |
Level.Iteration |
每次迭代都会运行 |
Level.Invocation |
每次方法调用都会运行 |
还有一些配置参数,例如禁止方法内联 @CompilerControl(CompilerControl.Mode.DONT_INLINE)
等,可以参考 jmh - samples | GitHub。
案例
某系统需要开发一个基于用户群组的待办事项清单模块,原理和效果类似使用生产者 - 消费者模型的消息队列,使用时需要进行大量的序列化(写)和更大量的反序列化(读)操作。目前使用的方案是 Jackson,亦有考虑使用 Protobuf。
创建性能测试
快速创建一个 JMH 工程:
mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=cc.ddrpa.showcase \
-DartifactId=object-serialization-benchmark \
-Dversion=1.0
创建的测试消息类如下,实际做性能测试时需要根据目标业务设计测试的数据结构,不同方案可能对特定类型的对象有奇效。
public class EventPayloadEntity implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long id;
private String name;
private String veryLongAndMultiLineDescription;
private EventStatus status;
private List<Address> addressBook;
private LocalDateTime startTime;
private Map<String, String> dictionary;
}
public enum EventStatus {
ACTIVE(1),
INACTIVE(2),
DELETED(10);
}
public class Address implements Serializable {
@Serial
private static final long serialVersionUID = 2L;
private String province;
private String city;
private String street;
}
Jackson 方案通过 ObjectMapper 实现对象和字符串之间的转换。Protobuf 方案需要编写 .proto
定义文件,通过 protoc
生成 Message 和 Builder 类的 Java 代码,编写 Helper
函数实现 POJO 和 Message 间的转换。
使用 mvn clean verify
产生 benchmarks.jar
进行性能测试。
# Detecting actual CPU count: 16 detected
# JMH version: 1.36
# VM version: JDK 17.0.5, OpenJDK 64-Bit Server VM, 17.0.5+8
# VM invoker: C:\Program Files\Eclipse Adoptium\jdk-17.0.5.8-hotspot\bin\java.exe
# VM options: <none>
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
# Warmup: 5 iterations, 3 s each
# Measurement: 5 iterations, 3 s each
# Timeout: 10 min per iteration
# Threads: 16 threads
# Benchmark mode: Throughput, ops/time
Benchmark Mode Cnt Score Error Units
BenchmarkRunner.jackson thrpt 25 3227.269 ± 151.785 ops/ms
BenchmarkRunner.jackson:jacksonDeserialize thrpt 25 1075.210 ± 28.830 ops/ms
BenchmarkRunner.jackson:jacksonSerialize thrpt 25 2152.059 ± 152.515 ops/ms
BenchmarkRunner.jdk thrpt 25 70.897 ± 3.908 ops/ms
BenchmarkRunner.jdk:jdkDeserialize thrpt 25 69.811 ± 3.844 ops/ms
BenchmarkRunner.jdk:jdkSerialize thrpt 25 1.086 ± 0.096 ops/ms
BenchmarkRunner.protobuf thrpt 25 6649.438 ± 152.138 ops/ms
BenchmarkRunner.protobuf:protobufDeserialize thrpt 25 3633.028 ± 113.231 ops/ms
BenchmarkRunner.protobuf:protobufSerialize thrpt 25 3016.410 ± 97.707 ops/ms
机器在运行时可能会受到多种因素影响,因此测试结果应关注相对值。在本次测试中使用 Protobuf 进行对象的反序列化,性能大约是 Jackson 方案的 3.3 倍。
对这方面有兴趣的话还可以尝试如下方案: