一、JMH 基础概念
JMH 简介
JMH(Java Microbenchmark Harness)是 OpenJDK 团队开发的一个专门用于 Java 微基准测试的工具套件。它旨在帮助开发者编写、运行和分析精确的 Java 微基准测试,避免常见的性能测试陷阱。
为什么需要 JMH
在 Java 中直接使用 System.currentTimeMillis()
或 System.nanoTime()
进行性能测试存在诸多问题:
- JVM 预热:JVM 需要时间进行 JIT 编译优化
- 死代码消除:JVM 可能会优化掉未使用的计算结果
- 编译器优化:循环可能被展开或重排序
- 缓存效应:局部性原理会影响测试结果
- 线程调度:操作系统线程调度带来的不确定性
JMH 通过精心设计的测试框架解决了这些问题。
JMH 核心特性
- 自动预热:确保测试在 JIT 编译优化后进行
- 防止死代码消除:通过"黑洞"技术确保代码被执行
- 统计处理:提供多种统计方式和结果输出
- 多模式支持:支持吞吐量、平均时间、采样时间等测试模式
- JVM 控制:可以控制 JVM 参数和分叉测试
基本使用示例
添加依赖
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
<scope>provided</scope>
</dependency>
简单基准测试
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@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 MyBenchmark {
@Benchmark
public int testMethod() {
int a = 1;
int b = 2;
return a + b;
}
}
运行测试
可以通过 main 方法运行:
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.build();
new Runner(opt).run();
}
或者使用命令行:
mvn clean install
java -jar target/benchmarks.jar
常用注解说明
@Benchmark
:标记基准测试方法@BenchmarkMode
:测试模式(Throughput, AverageTime, SampleTime 等)@Warmup
:预热配置@Measurement
:实际测量配置@Fork
:JVM 分叉次数@State
:定义测试状态范围@Setup
/@TearDown
:初始化和清理方法@Param
:参数化测试
结果解读
JMH 输出包含丰富信息:
- 分数:每次操作的平均时间或吞吐量
- 误差:置信区间
- 单位:纳秒/微秒/毫秒等
- 统计:最小值、最大值、百分位数等
最佳实践
- 避免在基准测试中创建对象:除非专门测试对象创建性能
- 使用 State 对象共享数据:而不是局部变量
- 合理设置预热次数:确保 JIT 优化完成
- 多次分叉测试:消除 JVM 启动差异
- 关注误差范围:而不仅仅是平均结果
常见陷阱
- 忽略死代码消除:确保测试代码有实际效果
- 测试环境不一致:确保测试机器负载稳定
- 测试时间过短:无法触发 JIT 优化
- 忽略 GC 影响:长时间测试可能需要考虑 GC
- 错误解释结果:纳秒级差异在实际应用中可能无关紧要
高级用法
- 使用 Profiler:分析热点方法
- 异步分析:结合 async-profiler
- JVM 参数调整:测试不同 JVM 设置的影响
- 多线程测试:评估并发性能
- 参数化测试:测试不同输入规模下的表现
JMH 是 Java 性能测试的事实标准工具,正确使用可以避免大多数微基准测试的陷阱,得到可靠的性能数据。
JMH 的设计目标
解决传统基准测试的痛点
JMH(Java Microbenchmark Harness)由 OpenJDK 团队开发,旨在解决以下传统基准测试的常见问题:
- JVM 优化干扰:JIT 编译、死代码消除(Dead Code Elimination)等优化可能导致测试结果失真。
- 预热缺失:未考虑 JVM 预热阶段(如热点代码编译)对性能的影响。
- 环境噪声:未隔离系统后台进程、GC 等外部因素对测试的干扰。
- 统计方法粗糙:简单循环计时缺乏科学的统计模型(如分位数、误差范围)。
核心设计原则
- 科学性与可重复性:通过多次迭代、预热阶段和统计方法确保结果可信。
- 对抗 JVM 优化:自动处理 JIT 编译、内联等优化对测试的影响。
- 易用性:提供注解驱动的 API,降低编写可靠基准测试的门槛。
JMH 的核心优势
1. 对抗 JVM 优化策略
- 防止死代码消除:通过返回"黑洞"(
Blackhole
)强制使用计算结果。 - 控制内联:支持
@Fork
隔离测试,避免跨方法内联干扰。 - 循环展开处理:自动调整操作次数以避免循环优化失真。
2. 完整的测试生命周期管理
@Benchmark
@Warmup(iterations = 3, time = 1) // 预热3轮,每轮1秒
@Measurement(iterations = 5, time = 1) // 正式测量5轮
@Fork(2) // 启动2个独立JVM进程
public void testMethod() {
// 被测代码
}
3. 丰富的测量模式
- 吞吐量模式(
Throughput
):单位时间内的操作次数(ops/ms)。 - 平均时间模式(
AverageTime
):单次操作的平均耗时(ms/op)。 - 采样模式(
SampleTime
):记录耗时分位数(如99%请求的响应时间)。 - 单次耗时模式(
SingleShotTime
):测量非迭代场景(如冷启动性能)。
4. 多维度结果输出
支持生成:
- 原始数据(JSON/CSV)
- 可视化对比报告
- 统计摘要(平均值、标准差、置信区间)
5. 与 Java 生态深度集成
- 自动适配不同 JVM 版本
- 支持 GraalVM 等特殊运行时
- 与 Profiler 工具(如
-prof perfasm
)联动分析汇编代码
对比手工测试的典型优势场景
手工测试缺陷 | JMH 解决方案 |
---|---|
System.currentTimeMillis() 计时不精确 | 使用纳秒级计时 + 统计校正 |
未考虑 JIT 编译阶段 | 自动预热 + 多进程隔离 |
测试结果波动大 | 多次迭代 + 剔除异常值 |
JMH 的应用场景
JMH(Java Microbenchmark Harness)是专门用于 Java 微基准测试的工具,适用于需要精确测量代码性能的场景。以下是其典型应用场景:
1. 优化关键代码路径
- 场景:当需要优化高频调用的小段代码(如算法、数据结构操作、锁竞争等)时,JMH 可以精确测量优化前后的性能差异。
- 示例:比较
ArrayList
和LinkedList
在随机访问时的性能差异。
2. 验证性能假设
- 场景:开发中常有一些性能假设(如“使用
StringBuilder
比字符串拼接更快”),JMH 可通过数据验证这些假设是否成立。 - 示例:测试
String.concat()
与StringBuilder.append()
的性能对比。
3. 评估 JVM 特性影响
- 场景:分析 JIT 编译、内联、逃逸分析等 JVM 优化对代码的影响。
- 示例:测试方法内联(
final
方法 vs 普通方法)对性能的提升效果。
4. 多线程性能测试
- 场景:测量并发代码(如锁、原子类、并发容器)在不同线程数下的吞吐量或延迟。
- 示例:对比
synchronized
和ReentrantLock
在高并发场景下的性能。
5. 库/框架选型
- 场景:在引入第三方库(如 JSON 解析库、网络库)时,通过 JMH 量化不同库的性能差异。
- 示例:比较
Jackson
、Gson
和Fastjson
的序列化速度。
6. JVM 版本或参数调优
- 场景:评估升级 JVM 版本或调整 JVM 参数(如堆大小、GC 策略)对代码性能的影响。
- 示例:测试 G1 GC 和 ZGC 对低延迟应用的适用性。
7. 避免基准测试陷阱
- 场景:解决手工测试中常见的陷阱(如未预热、忽略 JIT 优化、环境干扰等)。
- 示例:演示未预热时直接测试的误差(如首次调用 vs 稳定状态性能)。
注意事项
- 不适用场景:JMH 不适合测试大型系统(如分布式服务)的整体性能,此类场景应使用压力测试工具(如 JMeter)。
- 结果解读:需结合统计误差(如置信区间)分析数据,避免过度优化微小差异。
JMH 与其他性能测试工具对比
1. JMH 概述
JMH(Java Microbenchmark Harness)是由 OpenJDK 团队开发的专为 Java 设计的微基准测试工具。它通过高度优化的测试框架,解决了 JVM 层面的性能测试难题(如 JIT 优化、预热效应等),适用于测量小段代码的精确性能。
2. 常见性能测试工具对比
2.1 JMH vs JUnit/TestNG
特性 | JMH | JUnit/TestNG |
---|---|---|
测试目标 | 微观性能(纳秒级精度) | 功能正确性验证 |
JVM 优化处理 | 自动处理预热、消除死代码 | 无特殊处理 |
统计输出 | 提供吞吐量、平均耗时等指标 | 仅通过/失败断言 |
适用场景 | 算法对比、锁性能测试等 | 单元测试 |
2.2 JMH vs Caliper(已废弃)
特性 | JMH | Caliper |
---|---|---|
维护状态 | 活跃(OpenJDK 官方支持) | 谷歌弃用,不再更新 |
功能完整性 | 支持多模式(Throughput/AverageTime) | 功能较少 |
易用性 | 注解驱动,集成 Maven/Gradle | 配置复杂 |
2.3 JMH vs Apache Benchmark (ab)
特性 | JMH | ab |
---|---|---|
测试层级 | JVM 内部代码级性能 | HTTP 接口级压力测试 |
并发模型 | 支持线程间竞争测试 | 仅模拟多请求并发 |
输出维度 | 纳秒级耗时、GC 影响分析 | 请求速率、响应时间 |
2.4 JMH vs Gatling/LoadRunner
特性 | JMH | Gatling/LoadRunner |
---|---|---|
测试规模 | 单机微基准 | 分布式系统压力测试 |
关注点 | 代码片段性能瓶颈定位 | 系统整体吞吐量、稳定性 |
资源消耗 | 低(单进程) | 高(需集群模拟) |
3. JMH 的不可替代性
- JVM 特性适配:自动处理热点编译、避免冗余优化(如死代码消除)。
- 精确控制:通过
@Warmup
、@Measurement
控制预热和测量阶段。 - 多维度统计:支持百分位数(p90/p99)、误差范围等高级指标。
4. 示例:JMH 的典型使用场景
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Thread)
public class ArrayListVsLinkedList {
private List<Integer> arrayList;
private List<Integer> linkedList;
@Setup
public void setup() {
arrayList = new ArrayList<>();
linkedList = new LinkedList<>();
// 初始化数据
}
@Benchmark
public void testArrayList(Blackhole bh) {
bh.consume(arrayList.get(1000));
}
@Benchmark
public void testLinkedList(Blackhole bh) {
bh.consume(linkedList.get(1000));
}
}
5. 工具选型建议
- 选择 JMH 当:需要对比两种数据结构的访问性能、分析同步锁开销等微观场景。
- 选择其他工具当:测试 REST API 响应时间(Gatling)或验证多线程安全性(JUnit)。
JMH 核心组件
JMH(Java Microbenchmark Harness)是专门用于 Java 微基准测试的工具,其核心组件包括以下几个部分:
1. 注解(Annotations)
JMH 通过注解来定义基准测试的行为和配置。常见的注解包括:
@Benchmark
:标记一个方法为基准测试方法。@BenchmarkMode
:指定基准测试的模式(如吞吐量、平均时间、单次执行时间等)。@OutputTimeUnit
:指定测试结果的输出时间单位(如毫秒、微秒、纳秒等)。@State
:定义测试状态(如线程共享、线程独享等)。@Warmup
:配置预热阶段的参数(如预热次数、时间等)。@Measurement
:配置实际测量阶段的参数(如测量次数、时间等)。@Fork
:指定 JVM 的 fork 次数(用于隔离测试环境)。
2. 状态管理(State)
JMH 通过 @State
注解管理测试状态,支持以下作用域:
Scope.Thread
:每个线程独享一份状态。Scope.Benchmark
:所有线程共享同一份状态。Scope.Group
:用于线程组共享状态(结合@Group
使用)。
状态对象用于存储测试数据,避免因 JVM 优化(如常量折叠)影响测试结果。
3. 基准模式(Benchmark Modes)
JMH 支持多种基准测试模式,通过 @BenchmarkMode
指定:
Throughput
:测量单位时间内的操作次数(ops/time)。AverageTime
:测量每次操作的平均耗时(time/op)。SampleTime
:采样统计每次操作的耗时分布。SingleShotTime
:测量单次操作的耗时(适用于冷启动测试)。All
:同时启用所有模式。
4. 测试执行引擎(Runner)
JMH 通过 org.openjdk.jmh.runner.Runner
类执行基准测试,负责:
- 解析注解配置。
- 管理预热和测量阶段。
- 控制 JVM fork 和线程调度。
- 收集并输出测试结果。
5. 结果处理(Results)
JMH 提供丰富的测试结果输出格式,包括:
- 控制台输出(默认)。
- JSON、CSV 等文件格式。
- 支持自定义结果处理器(通过
@Output
注解配置)。
6. 工具集成(Tooling)
JMH 提供了一些辅助工具,例如:
JMH Gradle Plugin
:集成到 Gradle 构建工具中。JMH Maven Plugin
:集成到 Maven 构建工具中。- 命令行工具:支持通过命令行运行基准测试。
示例代码
以下是一个简单的 JMH 基准测试示例,展示了核心组件的使用:
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@State(Scope.Benchmark)
public class MyBenchmark {
private int value;
@Setup
public void setup() {
value = 42;
}
@Benchmark
public int testMethod() {
return value * value;
}
}
注意事项
- 避免死代码消除(Dead Code Elimination):确保基准测试方法的返回值被使用(如通过
Blackhole
类)。 - 控制 JVM 优化:避免常量折叠等优化影响测试结果。
- 合理配置预热和测量:确保 JVM 达到稳定状态后再进行测量。
- 隔离测试环境:通过
@Fork
避免测试间的相互干扰。
二、JMH 环境搭建
JMH 依赖配置
Maven 配置
在 Maven 项目中,需要在 pom.xml
中添加 JMH 的核心依赖和插件配置:
<dependencies>
<!-- JMH 核心依赖 -->
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version> <!-- 使用最新版本 -->
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- JMH 插件配置 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>benchmarks</finalName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Gradle 配置
在 Gradle 项目中,需要在 build.gradle
中添加以下配置:
plugins {
id 'java'
id 'me.champeau.jmh' version '0.7.2' // JMH 插件
}
dependencies {
// JMH 核心依赖
jmhImplementation 'org.openjdk.jmh:jmh-core:1.37'
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
}
jmh {
// 可选配置,如指定 JMH 版本
jmhVersion = '1.37'
// 其他配置项,如线程数、预热迭代次数等
iterations = 5
fork = 2
}
注意事项
- 版本一致性:确保
jmh-core
和jmh-generator-annprocess
的版本一致。 - 插件作用:
- Maven 的
maven-shade-plugin
用于打包可执行的 JMH 基准测试。 - Gradle 的
jmh
插件简化了 JMH 的配置和执行。
- Maven 的
- 注解处理器:
jmh-generator-annprocess
是注解处理器,需设置为provided
(Maven)或使用annotationProcessor
(Gradle)。 - 运行方式:
- Maven:通过
mvn clean package
打包后,使用java -jar target/benchmarks.jar
运行。 - Gradle:直接通过
gradle jmh
运行基准测试。
- Maven:通过
JMH 性能基准测试的基本项目结构
项目目录结构
一个典型的 JMH 基准测试项目通常包含以下核心目录和文件:
jmh-project/
├── src/
│ ├── main/
│ │ ├── java/ # 基准测试代码
│ │ └── resources/ # 配置文件(可选)
│ └── test/ # 常规单元测试(与JMH无关)
├── target/ # 编译输出目录
├── pom.xml # Maven配置文件
└── README.md
核心文件说明
1. pom.xml 配置
Maven 项目必须包含 JMH 依赖和插件:
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.37</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.37</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>benchmarks</finalName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
2. 基准测试类
标准结构示例:
package com.example;
import org.openjdk.jmh.annotations.*;
@State(Scope.Benchmark)
public class MyBenchmark {
@Setup
public void setup() {
// 初始化代码
}
@Benchmark
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public void testMethod() {
// 被测试的代码逻辑
}
@TearDown
public void tearDown() {
// 清理代码
}
}
关键注解说明
注解 | 作用域 | 说明 |
---|---|---|
@Benchmark | 方法 | 标记基准测试方法 |
@State | 类 | 定义测试状态(Scope.Benchmark/Thread/Group) |
@Setup | 方法 | 测试前的初始化操作 |
@TearDown | 方法 | 测试后的清理操作 |
@BenchmarkMode | 方法/类 | 指定测量模式(Throughput/AverageTime/SampleTime等) |
@OutputTimeUnit | 方法/类 | 指定结果时间单位(TimeUnit.SECONDS/MILLISECONDS等) |
运行方式
命令行运行
mvn clean package
java -jar target/benchmarks.jar
IDE 运行
需要添加主类配置:
public class BenchmarkRunner {
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
注意事项
- 基准测试类应该放在
src/main/java
目录下 - 避免将 JMH 测试与常规单元测试混用
- 推荐每个基准测试类专注于单一功能点的测试
- 测试方法应该保持无参且返回 void(除非测试返回值场景)
编写第一个 JMH 测试类
什么是 JMH 测试类
JMH(Java Microbenchmark Harness)是专门用于 Java 和其他 JVM 语言的微基准测试工具。JMH 测试类是一个包含基准测试方法的 Java 类,用于测量代码片段的性能表现。
基本结构
一个基本的 JMH 测试类包含以下部分:
@Benchmark
注解标记的测试方法main
方法或使用 JMH 运行器- 可选的配置注解(如
@State
,@Setup
,@TearDown
等)
示例代码
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 测试模式:平均执行时间
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 输出时间单位
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) // 预热设置
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 正式测量设置
@Fork(1) // 使用1个进程
@State(Scope.Thread) // 每个线程一个实例
public class MyFirstBenchmark {
@Setup
public void setup() {
// 初始化代码,每个基准测试前执行
}
@Benchmark
public void testMethod() {
// 被测试的代码
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
}
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
关键注解说明
@Benchmark
:标记要测试的方法@BenchmarkMode
:设置测试模式(吞吐量、平均时间等)@Warmup
:配置预热参数@Measurement
:配置正式测量参数@State
:定义测试状态的范围
运行方式
- 通过 main 方法运行
- 使用 Maven 插件运行(推荐):
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
注意事项
- 避免在测试方法中返回无用值(JMH 会优化掉)
- 确保测试代码足够简单,专注于要测量的部分
- 合理设置预热和测量迭代次数
- 考虑使用
@State
管理测试数据
进阶用法
可以添加参数化测试:
@Param({"10", "100", "1000"})
private int size;
@Benchmark
public void testWithParam() {
for (int i = 0; i < size; i++) {
// 测试代码
}
}
@Benchmark
定义
@Benchmark
是 JMH 中最核心的注解,用于标记需要进行性能测试的方法。被该注解标注的方法会被 JMH 自动识别并执行基准测试。
使用场景
- 当需要测量某个方法的执行时间或吞吐量时
- 比较不同算法或实现的性能差异
- 验证代码优化效果
示例代码
@Benchmark
public void testMethod() {
// 被测代码
}
注意事项
- 方法应该是公开的(public)
- 避免在方法内创建大量临时对象
- 方法应保持单一职责
@BenchmarkMode
定义
用于指定基准测试的测量模式,可以组合使用多种模式。
常用模式
Throughput
:测量吞吐量(ops/time)AverageTime
:测量平均执行时间SampleTime
:采样执行时间SingleShotTime
:单次执行时间All
:所有模式
示例代码
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.AverageTime})
public void testMethod() {
// 被测代码
}
@Warmup
定义
控制预热阶段的行为,JVM 需要预热才能达到最佳性能状态。
重要参数
iterations
:预热迭代次数time
:每次迭代持续时间timeUnit
:时间单位batchSize
:每批次操作数量
示例代码
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
public class MyBenchmark {
// ...
}
@Measurement
定义
控制实际测量阶段的行为。
参数说明
iterations
:测量迭代次数time
:每次迭代持续时间timeUnit
:时间单位
示例代码
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS)
public class MyBenchmark {
// ...
}
@Fork
定义
指定测试进程的 fork 数量,用于减少 JVM 优化带来的影响。
参数说明
value
:fork 的进程数量warmups
:预热 fork 数量
示例代码
@Fork(value = 3, warmups = 1)
public class MyBenchmark {
// ...
}
@State
定义
定义测试状态,用于管理测试数据。
状态类型
Scope.Benchmark
:所有线程共享Scope.Thread
:线程私有Scope.Group
:线程组共享
示例代码
@State(Scope.Benchmark)
public class MyState {
public int value = 42;
}
public class MyBenchmark {
@Benchmark
public void testMethod(MyState state) {
// 使用 state.value
}
}
@Setup 和 @TearDown
定义
@Setup
:在基准测试前执行初始化@TearDown
:在基准测试后执行清理
执行时机
Level.Trial
:整个测试前后Level.Iteration
:每次迭代前后Level.Invocation
:每次方法调用前后
示例代码
@State(Scope.Thread)
public class MyState {
public List<String> data;
@Setup(Level.Trial)
public void init() {
data = new ArrayList<>();
}
@TearDown(Level.Trial)
public void cleanup() {
data = null;
}
}
@Param
定义
用于参数化基准测试,可以测试不同参数下的性能表现。
示例代码
@State(Scope.Benchmark)
public class MyState {
@Param({"10", "100", "1000"})
public int size;
public int[] array;
@Setup
public void setup() {
array = new int[size];
}
}
public class MyBenchmark {
@Benchmark
public void testMethod(MyState state) {
Arrays.sort(state.array);
}
}
@Threads
定义
指定并发线程数,用于测试多线程性能。
示例代码
@Benchmark
@Threads(4)
public void testMethod() {
// 并发测试代码
}
@OutputTimeUnit
定义
指定测试结果的时间单位。
示例代码
@Benchmark
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public void testMethod() {
// 被测代码
}
运行 JMH 测试的几种方式
JMH(Java Microbenchmark Harness)是一个专门用于 Java 和其他 JVM 语言的微基准测试框架。以下是运行 JMH 测试的几种常见方式:
1. 通过 Maven 插件运行
JMH 提供了一个 Maven 插件,可以通过 Maven 命令直接运行基准测试。
配置 Maven 插件
在 pom.xml
中添加 JMH 插件配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>benchmarks</finalName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
运行测试
使用以下命令运行所有基准测试:
mvn clean package
java -jar target/benchmarks.jar
可以指定特定的测试类或方法:
java -jar target/benchmarks.jar MyBenchmark
2. 通过 IDE 运行
可以直接在 IDE(如 IntelliJ IDEA 或 Eclipse)中运行 JMH 测试。
创建主类
编写一个包含 main
方法的主类来启动 JMH:
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
public class BenchmarkRunner {
public static void main(String[] args) throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
运行
在 IDE 中直接运行 BenchmarkRunner
类。
3. 通过命令行直接运行
如果已经将 JMH 测试打包为可执行的 JAR 文件,可以直接通过命令行运行:
java -jar benchmarks.jar
可以指定参数来控制测试行为,例如:
java -jar benchmarks.jar -wi 5 -i 5 -f 1
-wi
:预热迭代次数-i
:测量迭代次数-f
:fork 次数
4. 通过 Gradle 运行
如果使用 Gradle 构建项目,可以通过 Gradle 插件运行 JMH 测试。
配置 Gradle 插件
在 build.gradle
中添加 JMH 插件:
plugins {
id 'java'
id 'me.champeau.jmh' version '0.6.6'
}
dependencies {
jmhImplementation 'org.openjdk.jmh:jmh-core:1.35'
jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess:1.35'
}
运行测试
使用以下命令运行所有基准测试:
gradle jmh
可以指定特定的测试类:
gradle jmh --include 'MyBenchmark'
5. 通过 JUnit 集成运行
JMH 可以与 JUnit 集成,方便在单元测试中运行基准测试。
添加依赖
在 pom.xml
中添加 JUnit 和 JMH 依赖:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
<scope>test</scope>
</dependency>
编写测试类
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.junit.Test;
public class MyBenchmarkTest {
@Test
public void runBenchmarks() throws RunnerException {
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.forks(1)
.build();
new Runner(opt).run();
}
}
运行
在 IDE 或构建工具中运行 MyBenchmarkTest
类。
注意事项
- 避免在 IDE 中直接运行
@Benchmark
方法:这会导致结果不准确,因为 JMH 需要控制预热和测量迭代。 - 选择合适的运行方式:对于简单的测试,IDE 或命令行运行足够;对于复杂项目,建议使用 Maven 或 Gradle 插件。
- 配置参数:根据测试需求调整预热次数、测量次数和 fork 次数,以获得更准确的结果。
三、JMH 注解详解
@Benchmark 基准测试方法
概念定义
@Benchmark
是 JMH(Java Microbenchmark Harness)框架中的核心注解,用于标记一个方法作为基准测试方法。JMH 是由 OpenJDK 团队开发的专门用于 Java 微基准测试的工具,能够避免 JVM 优化(如死代码消除、循环展开等)对测试结果的影响,提供准确的性能数据。
使用场景
- 微基准测试:测量小段代码的性能,例如比较不同算法的执行效率。
- 优化验证:验证代码优化后的性能提升是否达到预期。
- JVM 特性研究:研究 JVM 的即时编译(JIT)、内联等优化行为对性能的影响。
常见误区或注意事项
- 避免死代码消除:JMH 会自动处理死代码消除问题,但测试方法中应尽量返回计算结果,避免 JVM 优化掉无用代码。
- 预热阶段:基准测试前会有预热阶段(Warmup),确保 JIT 编译完成,结果稳定。
- 状态管理:使用
@State
注解管理测试状态,避免共享状态影响结果。 - 避免循环:不要在
@Benchmark
方法内手动添加循环,JMH 会控制迭代次数。
示例代码
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime) // 测试模式:平均时间
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 输出时间单位:纳秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 预热 5 次,每次 1 秒
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 正式测量 5 次,每次 1 秒
@Fork(1) // 使用 1 个 JVM 进程
public class MyBenchmark {
@Benchmark
public int testMethod() {
// 被测代码
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
return sum; // 返回结果避免死代码消除
}
public static void main(String[] args) throws Exception {
org.openjdk.jmh.Main.main(args);
}
}
关键点说明
@BenchmarkMode
:指定测试模式,如Mode.Throughput
(吞吐量)、Mode.AverageTime
(平均时间)。@OutputTimeUnit
:定义结果的时间单位(如纳秒、微秒、毫秒)。@Warmup
和@Measurement
:分别配置预热和正式测量的迭代次数与时间。@Fork
:指定 JVM 进程数量,避免测试间相互干扰。
@Warmup 预热设置
概念定义
@Warmup
是 JMH(Java Microbenchmark Harness)中的一个注解,用于配置基准测试的预热阶段。预热阶段是指在正式测量性能之前,先运行若干次基准测试方法,目的是让 JVM 的即时编译器(JIT)完成代码优化、缓存预热等操作,从而避免冷启动对性能测试结果的影响。
主要参数
@Warmup
注解支持以下关键参数:
- iterations:预热阶段的迭代次数(默认值:
5
)。 - time:每次预热迭代的持续时间(默认值:
10
秒)。 - timeUnit:预热时间的单位(默认值:
TimeUnit.SECONDS
)。 - batchSize:每次迭代中调用基准方法的次数(默认值:
1
,适用于单次调用场景)。
使用场景
- JIT 优化:确保热点代码被 JIT 编译优化。
- 缓存预热:填充 CPU 缓存、分支预测器等硬件缓存。
- 消除冷启动偏差:避免首次运行因类加载、初始化等操作导致的性能波动。
示例代码
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
@Benchmark
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
public void testMethod() {
// 基准测试逻辑
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
}
}
常见误区与注意事项
- 预热不足:迭代次数或时间过短可能导致 JIT 未完全优化,测试结果不稳定。
- 过度预热:过多的预热会延长测试时间,但可能不会带来额外的优化效果。
- 忽略批处理模式:对于高频小操作,可通过
batchSize
模拟真实调用压力。 - 环境一致性:确保预热和正式测试时的 JVM 状态(如 CPU 频率、内存)一致。
高级建议
- 生产环境匹配:预热参数应尽量模拟实际生产环境的调用模式。
- 动态调整:结合
@Fork
注解多次运行测试,观察预热效果是否稳定。
@Measurement 测量设置
概念定义
@Measurement
是 JMH(Java Microbenchmark Harness)中的一个注解,用于配置基准测试的测量阶段(Measurement Phase)的参数。测量阶段是 JMH 执行实际性能测试的阶段,用于收集性能数据。
主要参数
@Measurement
注解包含以下关键参数:
- iterations:测量阶段的迭代次数(默认为 5)。
- time:每次迭代的持续时间(默认为 1 秒)。
- timeUnit:时间单位(默认为
TimeUnit.SECONDS
)。 - batchSize:每次操作调用的批量大小(默认为 1,表示单次调用)。
使用场景
@Measurement
通常用于:
- 调整基准测试的测量精度和稳定性。
- 控制测试的总执行时间(通过
iterations
和time
的组合)。 - 模拟真实场景中的批量操作(通过
batchSize
)。
示例代码
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
@Benchmark
@Measurement(iterations = 10, time = 200, timeUnit = TimeUnit.MILLISECONDS)
public void testMethod() {
// 被测代码
}
}
- 此示例中,测量阶段会执行 10 次迭代,每次迭代持续 200 毫秒。
常见误区与注意事项
- 测量时间过短:如果
time
设置得太小(如 1 毫秒),可能导致 JVM 无法充分优化或预热,结果不准确。 - 迭代次数过多:过多的
iterations
会延长测试时间,但可能不会显著提高结果精度。 - 忽略
batchSize
:对于高频小操作,合理设置batchSize
可以减少测量开销。 - 与
@Warmup
混淆:@Measurement
是测量阶段,而@Warmup
是预热阶段,两者参数独立。
最佳实践
- 通常建议
time
不小于 100 毫秒,以减少测量误差。 - 结合
@Warmup
和@Fork
注解,确保测试环境稳定。 - 通过多次调整参数,观察结果稳定性。
@Fork 进程设置
概念定义
@Fork
是 JMH(Java Microbenchmark Harness)中的一个注解,用于控制基准测试的进程(JVM)相关配置。它允许用户指定基准测试运行时的 JVM 进程数量、预热次数和测量次数等参数。通过 @Fork
,可以隔离不同测试之间的干扰,确保每次测试都在一个干净的 JVM 环境中运行。
使用场景
- 隔离测试环境:每个
@Fork
启动一个独立的 JVM 进程,避免测试间的相互影响(如类加载、JIT 编译等)。 - 多进程并行测试:通过设置
value
参数,可以启动多个 JVM 进程并行运行测试,提高测试效率。 - 自定义 JVM 参数:通过
jvmArgs
参数可以为每个进程指定特定的 JVM 选项(如堆大小、GC 策略等)。
常用参数
参数 | 说明 | 默认值 |
---|---|---|
value | 启动的 JVM 进程数量 | 1 |
warmups | 每个进程的预热次数(不记录结果) | 0 |
jvmArgs | 传递给 JVM 的命令行参数 | 空数组 |
jvmArgsAppend | 追加到默认 JVM 参数的额外参数 | 空数组 |
jvmArgsPrepend | 预置到默认 JVM 参数的额外参数 | 空数组 |
示例代码
import org.openjdk.jmh.annotations.*;
@Fork(
value = 3, // 启动 3 个 JVM 进程
warmups = 1, // 每个进程预热 1 次
jvmArgs = {"-Xms2G", "-Xmx2G"} // 设置 JVM 堆大小
)
@BenchmarkMode(Mode.AverageTime)
public class MyBenchmark {
@Benchmark
public void testMethod() {
// 基准测试代码
}
}
常见误区
- 进程数过多:
value
设置过大会消耗大量系统资源,可能导致测试变慢或机器卡顿。 - 忽略预热:未设置
warmups
可能导致测试结果不稳定(JIT 未充分优化)。 - JVM 参数冲突:
jvmArgs
覆盖默认参数时需注意兼容性(如-server
模式)。
注意事项
- 资源分配:根据机器 CPU 核心数合理设置
value
(建议不超过核心数)。 - 预热必要性:对于需要 JIT 优化的代码,至少设置
warmups=1
。 - 参数验证:通过
-prof gc
等分析工具确认 JVM 参数是否生效。
@Threads 线程设置
概念定义
@Threads
是 JMH(Java Microbenchmark Harness)中的一个注解,用于指定基准测试运行时使用的线程数量。它允许开发者模拟多线程环境下的性能表现,测试代码在不同并发级别下的吞吐量、延迟等指标。
使用场景
- 并发性能测试:评估代码在高并发场景下的表现。
- 资源竞争分析:测试锁、同步机制或并发容器的性能。
- 线程数调优:确定最优线程数(如线程池大小配置)。
参数说明
value
:指定线程数量,支持以下形式:- 固定值(如
@Threads(4)
)。 - 动态值(如
@Threads(Threads.MAX)
,使用最大可用线程数)。
- 固定值(如
- 默认值:
@Threads(1)
(单线程模式)。
示例代码
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
public class MyBenchmark {
@Benchmark
@Threads(4) // 使用4个线程运行测试
public void testMethod() {
// 被测代码逻辑
}
@Benchmark
@Threads(Threads.MAX) // 使用所有可用线程
public void testMethodMaxThreads() {
// 被测代码逻辑
}
}
注意事项
- 线程数与硬件资源:
线程数不应超过物理核心数(避免过度切换),但可测试超线程场景。 - 预热阶段:
多线程测试需更长的预热(@Warmup
)以消除 JIT 和线程调度的影响。 - 同步代码块:
若测试含锁的代码,需结合@Group
和@GroupThreads
模拟更真实的竞争。 - 结果解读:
吞吐量(Throughput)可能随线程数增加先升后降,需分析临界点。
常见误区
- 盲目增加线程数:
更多线程不意味着更高性能,需观察曲线拐点。 - 忽略线程安全:
基准方法本身需保证线程安全,避免测试结果被干扰。 - 单次测试结论:
应结合不同线程数的多次测试对比分析。
@State 状态管理
概念定义
@State
是 JMH(Java Microbenchmark Harness)中的一个核心注解,用于声明基准测试中的共享状态。它标记的类实例会在多个基准测试方法之间共享,确保测试环境的一致性。JMH 会为每个线程或线程组创建独立的状态实例,避免并发竞争问题。
使用场景
- 共享测试数据:当多个基准方法需要操作同一组数据时(如大型数组、集合)。
- 资源初始化:数据库连接、文件句柄等昂贵资源的初始化。
- 多线程测试:模拟真实并发场景下的状态共享。
常见模式
@State(Scope.Thread) // 作用域为线程私有
public class MyState {
public int[] data;
@Setup(Level.Trial) // 整个测试前初始化
public void init() {
data = new int[1000];
Arrays.fill(data, 42);
}
}
@Benchmark
public void testMethod(MyState state) {
// 使用 state.data 进行操作
}
作用域类型
作用域 | 说明 |
---|---|
Scope.Thread | 每个线程创建独立实例(默认) |
Scope.Benchmark | 所有线程共享同一实例 |
Scope.Group | 线程组内共享实例(需配合@Group 使用) |
生命周期方法
@State(Scope.Thread)
public class LifecycleState {
@Setup(Level.Trial) // 整个基准测试前执行
public void globalSetup() { /* ... */ }
@Setup(Level.Iteration) // 每次测试迭代前执行
public void iterationSetup() { /* ... */ }
@TearDown(Level.Trial) // 整个基准测试后执行
public void globalTearDown() { /* ... */ }
}
注意事项
- 线程安全:
Scope.Benchmark
作用域的状态类必须保证线程安全 - 避免耗时操作:
@Setup
中的初始化不应影响基准测试结果 - 状态传递:只能通过方法参数注入状态实例(不可手动实例化)
- 默认值问题:状态类字段会被自动初始化为默认值(0/null/false)
典型错误示例
// 错误1:尝试手动实例化状态类
@Benchmark
public void wrongTest() {
MyState state = new MyState(); // 违反JMH规则
}
// 错误2:非线程安全的状态共享
@State(Scope.Benchmark)
public class UnsafeState {
public int counter = 0; // 多线程并发写会出错
}
最佳实践
- 优先使用
Scope.Thread
保证线程隔离 - 对于只读数据可使用
Scope.Benchmark
减少内存开销 - 复杂状态对象应实现
Serializable
接口(支持JMH的fork选项)
@Param 参数化测试
概念定义
@Param
是 JMH(Java Microbenchmark Harness)中的一个注解,用于实现参数化基准测试。它允许你在运行基准测试时动态传入不同的参数值,从而测试同一段代码在不同输入条件下的性能表现。
使用场景
- 多条件性能对比:比如测试不同大小的集合(如
ArrayList
和LinkedList
)在不同数据量下的性能差异。 - 配置调优:比如测试不同线程池大小或缓存容量对性能的影响。
- 算法优化:比如比较不同算法实现在不同输入规模下的耗时。
注意事项
- 参数类型限制:
@Param
仅支持基本类型(如int
、String
)或枚举类型。复杂对象需要通过@Setup
方法初始化。 - 参数组合爆炸:如果定义多个
@Param
字段,JMH 会测试所有可能的组合,可能导致测试时间过长。 - JVM 预热影响:参数化测试可能因 JVM 的即时编译(JIT)优化导致某些参数组合的结果失真,需确保充分预热。
示例代码
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ParamBenchmark {
@Param({"10", "100", "1000"}) // 定义参数值
private int size;
private int[] array;
@Setup
public void setup() {
array = new int[size];
for (int i = 0; i < size; i++) {
array[i] = i;
}
}
@Benchmark
public int sum() {
int sum = 0;
for (int x : array) {
sum += x;
}
return sum;
}
}
运行结果解析
运行上述测试后,JMH 会生成类似如下的报告:
Benchmark (size) Mode Cnt Score Error Units
ParamBenchmark.sum 10 avgt 5 15.234 ± 0.123 ns/op
ParamBenchmark.sum 100 avgt 5 142.567 ± 1.456 ns/op
ParamBenchmark.sum 1000 avgt 5 1450.789 ± 12.345 ns/op
- 每行对应一个参数组合的测试结果。
Score
列显示平均耗时,可直观比较不同size
的性能差异。
高级用法
- 多参数组合:声明多个
@Param
字段测试交叉组合:@Param({"1", "10"}) private int threads; @Param({"256", "1024"}) private int bufferSize;
- 枚举参数:使用枚举类型更清晰地定义参数:
public enum Mode { FAST, SLOW } @Param private Mode mode;
@OutputTimeUnit 时间单位
概念定义
@OutputTimeUnit
是 JMH(Java Microbenchmark Harness)性能基准测试框架中的一个注解,用于指定基准测试结果的时间单位。它允许开发者自定义测试结果的输出格式,便于在不同粒度下比较和分析性能数据。
支持的常用时间单位
JMH 支持以下 java.util.concurrent.TimeUnit
枚举值:
NANOSECONDS
(纳秒)MICROSECONDS
(微秒)MILLISECONDS
(毫秒)SECONDS
(秒)MINUTES
(分钟)等。
使用场景
- 统一结果单位:在多组测试中强制使用相同的时间单位,避免手动转换。
- 可读性优化:根据测试的预期耗时选择合适单位(例如,短耗时用纳秒,长耗时用秒)。
- 报告标准化:生成报告时保持单位一致性。
示例代码
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 指定输出单位为纳秒
public class MyBenchmark {
@Benchmark
public void testMethod() {
// 被测代码逻辑
}
}
注意事项
- 单位选择:避免对长耗时操作使用
NANOSECONDS
,可能导致数值过大(如1秒=1,000,000,000纳秒
)。 - 与
@BenchmarkMode
的关联:仅对时间相关的模式(如Mode.AverageTime
、Mode.SampleTime
)有效。 - 默认值:未指定时,JMH 默认使用
SECONDS
。
常见误区
- 混淆时间单位与统计模式:
@OutputTimeUnit
仅控制输出格式,不改变实际的测量方式(如采样频率)。 - 忽略单位转换误差:在对比不同单位的测试结果时,需注意精度损失(如微秒到毫秒的截断)。
@BenchmarkMode 测试模式
概念定义
@BenchmarkMode
是 JMH(Java Microbenchmark Harness)性能测试框架中的一个核心注解,用于指定基准测试的测量模式。它定义了测试结果的展示方式,帮助开发者从不同维度评估代码性能。
支持的测试模式
JMH 提供了以下五种基准模式(通过 Mode
枚举类定义):
-
Throughput(吞吐量模式)
- 默认模式
- 测量单位时间内操作执行的次数(ops/time)
- 结果示例:
123456.789 ops/s
-
AverageTime(平均时间模式)
- 测量单次操作的平均耗时
- 结果示例:
0.123 ns/op
-
SampleTime(采样时间模式)
- 统计单次操作的耗时分布(包括百分位数)
- 可显示类似
p(99.9%)=10.5ms
的分布数据
-
SingleShotTime(单次执行时间模式)
- 测量单次操作的耗时(不进行预热迭代)
- 适用于冷启动性能测试
-
All(全模式)
- 同时启用以上所有模式
使用示例
@Benchmark
@BenchmarkMode({Mode.Throughput, Mode.AverageTime}) // 组合模式
public void testMethod() {
// 被测代码
}
注意事项
-
模式选择依据
- 需要评估吞吐量时(如API性能)优先选
Throughput
- 需要评估延迟时(如算法耗时)优先选
AverageTime
或SampleTime
- 测试JVM冷启动性能时使用
SingleShotTime
- 需要评估吞吐量时(如API性能)优先选
-
结果单位
- 时间单位自动适配(ns/us/ms/s)
- 可通过
@OutputTimeUnit
注解强制指定单位(例如TimeUnit.MILLISECONDS
)
-
模式组合
- 可同时指定多个模式(如示例代码所示)
- 使用
Mode.All
会显著增加测试时间
典型误区
-
混淆模式与测试目标
- 错误示例:在测试算法延迟时仅使用
Throughput
模式 - 正确做法:根据核心指标选择对应模式(延迟用
AverageTime
,吞吐量用Throughput
)
- 错误示例:在测试算法延迟时仅使用
-
忽略模式间的关联性
Throughput
和AverageTime
本质是互为倒数关系- 公式:
吞吐量 = 1 / 平均耗时
-
单次执行模式的误用
SingleShotTime
不进行JVM预热,结果可能包含JIT编译等干扰- 常规测试应配合
@Warmup
注解使用其他模式
四、JMH 测试模式
Throughput 吞吐量模式
概念定义
Throughput(吞吐量)模式是 JMH(Java Microbenchmark Harness)性能基准测试中的一种核心测量模式,用于衡量单位时间内被测方法的调用次数。其计量单位为 ops/time(例如 ops/ms、ops/s),表示每毫秒或每秒能执行多少次操作。
核心特点
- 时间导向:关注方法在固定时间窗口内的执行次数。
- 高压力场景:通过持续调用方法模拟高并发或密集计算场景。
- 默认模式:JMH 的
@BenchmarkMode(Mode.Throughput)
会默认启用此模式。
使用场景
- 需要评估方法的最大处理能力(如 API 接口的 QPS)
- 对比不同算法在持续负载下的性能差异
- 验证代码优化后的吞吐量提升效果
示例代码
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.Throughput) // 显式声明吞吐量模式
@OutputTimeUnit(TimeUnit.SECONDS) // 指定输出单位为秒
public class ThroughputBenchmark {
@Benchmark
public void testMethod() {
// 被测方法逻辑(示例:字符串拼接)
String result = "JMH" + System.currentTimeMillis();
}
}
输出结果解读
运行后会得到类似如下格式的结果:
Benchmark Mode Cnt Score Error Units
ThroughputBenchmark.testMethod thrpt 5 356789.12 ± 1.5% ops/s
- Score: 356789.12 ops/s 表示每秒可执行约 35.6 万次操作
- Error: ±1.5% 表示测量误差范围
注意事项
- 预热重要性:必须通过
@Warmup
充分预热,避免 JIT 编译干扰结果 - 防止死代码消除:需通过
Blackhole
消费返回值或添加@CompilerControl
注解 - 线程数影响:通过
@Threads
设置合理线程数(默认 1 线程) - 时间单位选择:根据实际场景选择秒(s)、毫秒(ms)或微秒(us)
对比其他模式
模式 | 关注点 | 典型单位 |
---|---|---|
Throughput | 单位时间操作量 | ops/s |
AverageTime | 单次操作平均耗时 | ns/op |
SampleTime | 耗时分布百分比 | ns/op |
高级配置
通过 @Measurement
控制测试参数:
@Measurement(
iterations = 3, // 测量3轮
time = 10, // 每轮持续10秒
timeUnit = TimeUnit.SECONDS
)
AverageTime 平均时间模式
概念定义
AverageTime 是 JMH(Java Microbenchmark Harness)性能基准测试中的一种测量模式,用于计算被测方法每次调用的平均执行时间。该模式通过多次迭代执行目标方法,统计总耗时后计算单次调用的平均时间,结果单位为"时间/操作"(如 ns/op)。
核心特点
- 测量单位:默认纳秒/操作(ns/op),可通过
@OutputTimeUnit
注解修改 - 统计方式:总时间 / 操作次数
- 适用场景:适合测量非冷启动状态下的稳定性能表现
典型使用方式
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class MyBenchmark {
@Benchmark
public void testMethod() {
// 被测代码
}
}
数据采集过程
- 预热阶段(Warmup):不计入最终统计
- 执行阶段(Measurement):
- 固定时间窗口内连续调用目标方法
- 记录总耗时和操作次数
- 计算:平均时间 = 总耗时 / 操作次数
注意事项
- 长尾效应:可能掩盖个别异常耗时操作的影响
- 批处理优化:JIT 可能对循环中的多次调用进行优化
- 结果解读:应与 Throughput 模式结合分析
- 配置建议:
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
对比其他模式
模式 | 关注点 | 单位 | 适用场景 |
---|---|---|---|
AverageTime | 单次操作耗时 | ns/op | 延迟敏感型操作 |
Throughput | 单位时间操作量 | ops/s | 高吞吐场景 |
SampleTime | 时间分布 | ns | 分析耗时分布 |
典型输出示例
Benchmark Mode Cnt Score Error Units
MyBenchmark.testMethod avgt 5 23.456 ± 1.234 ns/op
(Score 表示平均耗时,± Error 表示误差范围)
SampleTime 采样时间模式
概念定义
SampleTime 是 JMH(Java Microbenchmark Harness)中的一种时间测量模式,用于统计基准测试方法在固定时间窗口内的执行次数。JMH 通过采样(Sampling)的方式,记录在特定时间段内方法能够执行的次数,从而计算出平均每次调用的耗时。
工作原理
- 时间窗口:JMH 会设定一个采样时间(默认为 1 秒),在该时间段内尽可能多地执行被测方法。
- 统计执行次数:记录在时间窗口内方法被调用的总次数。
- 计算平均耗时:通过
总时间 / 执行次数
得到单次调用的平均时间。
使用场景
- 短耗时方法:适合测试执行时间非常短(纳秒或微秒级)的方法。
- 吞吐量测试:关注方法在单位时间内的调用次数(如 ops/秒)。
- 避免冷启动误差:通过长时间采样减少 JVM 预热、JIT 编译等因素的影响。
配置参数
在 JMH 中可以通过注解配置 SampleTime 模式:
@BenchmarkMode(Mode.SampleTime) // 设置为采样时间模式
@OutputTimeUnit(TimeUnit.NANOSECONDS) // 指定输出时间单位
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) // 预热配置
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 测量配置
public class MyBenchmark {
@Benchmark
public void testMethod() {
// 被测代码
}
}
注意事项
- 与 Throughput 模式的区别:
- SampleTime 输出的是单次操作的平均耗时。
- Throughput 输出的是单位时间的操作次数(如 ops/秒)。
- 时间单位选择:建议根据实际耗时选择合适的时间单位(如纳秒、微秒或毫秒)。
- 采样时间设置:过短的采样时间可能导致统计不准确,一般建议 ≥1 秒。
示例输出
运行后会得到类似以下结果:
Result "com.example.MyBenchmark.testMethod":
2.456 ±(99.9%) 0.123 ns/op [Average]
(min, avg, max) = (2.100, 2.456, 3.200), stdev = 0.45
CI (99.9%): [2.333, 2.579] (assumes normal distribution)
其中:
2.456 ns/op
表示每次操作平均耗时 2.456 纳秒± 0.123 ns/op
表示置信区间误差范围
SingleShotTime 单次执行时间模式
概念定义
SingleShotTime 是 JMH(Java Microbenchmark Harness)中的一种基准测试模式,专门用于测量单次方法调用的执行时间。与其他模式(如 Throughput、AverageTime)不同,它不进行预热(warm-up)或多次迭代取平均值,而是直接记录单次执行的耗时。
使用场景
- 冷启动性能测试:适用于测量 JVM 冷启动时方法的初始执行时间(如类加载、JIT 编译前的性能)。
- 初始化开销分析:检测一次性初始化操作的耗时(如数据库连接建立、缓存填充)。
- 极端延迟场景:评估方法在最坏情况下的响应时间(如未优化的代码路径)。
注意事项
- 结果波动性:由于不预热且仅单次执行,结果易受 JVM 冷启动、GC 等因素影响,通常需多次运行观察分布。
- 不适用常规优化:不能反映 JIT 编译后的稳定性能,需结合
@Warmup
或其他模式综合评估。 - 避免误读:单次时间可能包含偶发噪声(如线程调度),需通过
@Measurement(iterations = N)
增加样本量。
示例代码
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class SingleShotBenchmark {
@State(Scope.Thread)
public static class MyState {
public int value;
@Setup(Level.Invocation)
public void setup() {
value = 0; // 每次调用前重置状态
}
}
@Benchmark
public int testMethod(MyState state) {
// 模拟耗时操作
for (int i = 0; i < 100_000; i++) {
state.value += i;
}
return state.value;
}
}
参数配置建议
@Measurement
:通过iterations
设置多次单次执行(如iterations = 10
)。@Fork
:配合多进程运行(如@Fork(3)
)减少 JVM 实例间干扰。- 状态管理:使用
@Setup(Level.Invocation)
确保每次调用前重置环境。
All 所有模式组合
概念定义
在 JMH(Java Microbenchmark Harness)性能基准测试框架中,"All 所有模式组合"指的是同时启用所有基准测试模式(Mode)的组合运行方式。JMH 提供了多种基准模式,每种模式测量不同的性能指标:
- Throughput(吞吐量模式):测量单位时间内操作执行的次数(ops/time)
- AverageTime(平均时间模式):测量单次操作的平均耗时(time/op)
- SampleTime(采样时间模式):测量单次操作的耗时分布
- SingleShotTime(单次执行时间模式):测量单次操作的耗时(无预热)
- All(所有模式):同时运行以上所有模式
使用场景
当您需要全面了解被测方法的性能特征时,可以使用 All 模式组合:
- 性能分析初期:快速获取方法的全方位性能数据
- 对比测试:需要同时比较吞吐量和延迟指标时
- 性能调优:观察不同优化手段对各种指标的影响
示例代码
@BenchmarkMode(Mode.All) // 使用All模式组合
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class AllModesBenchmark {
@Benchmark
public void testMethod() {
// 被测方法实现
}
}
注意事项
- 测试时间:All 模式会显著增加测试时间,因为要运行所有子模式
- 结果解读:不同模式的结果需要分别分析,不能直接比较
- 资源消耗:会占用更多系统资源,可能影响测试准确性
- 报告复杂度:生成的结果报告会包含所有模式的数据,较为复杂
常见误区
- 误认为All模式是"最佳"模式:实际上应根据具体需求选择合适模式
- 忽略模式间的差异:不同模式使用不同的测试方法,结果不能简单对比
- 过度依赖All模式:对于长期运行的基准测试,建议分开测试不同模式
推荐实践
- 初步探索时使用All模式快速了解性能特征
- 确定重点关注的指标后,改用特定模式进行详细测试
- 生产环境基准测试建议分开运行不同模式,以获得更准确的结果
五、JMH 高级特性
状态对象的使用
概念定义
状态对象(State Object)是一种设计模式,用于封装对象在不同状态下的行为。通过将状态抽象为独立的对象,可以在运行时动态改变对象的行为,而不必修改其类定义。状态对象通常与状态模式(State Pattern)结合使用,使得对象的行为随着其内部状态的改变而改变。
使用场景
- 对象行为依赖于状态:当对象的行为需要根据其内部状态动态变化时,可以使用状态对象。
- 避免条件分支:如果代码中存在大量的条件分支(如
if-else
或switch
语句)来检查对象状态,状态对象可以简化代码结构。 - 状态转换逻辑复杂:当状态转换逻辑复杂或状态较多时,状态对象可以提高代码的可维护性和可扩展性。
常见误区或注意事项
- 状态对象的生命周期:确保状态对象的生命周期管理得当,避免内存泄漏或频繁创建销毁对象。
- 状态转换的清晰性:状态转换逻辑应清晰明确,避免状态混乱或不可预测的行为。
- 线程安全性:在多线程环境中使用状态对象时,需确保状态转换的线程安全性。
示例代码
以下是一个简单的状态对象示例,模拟一个订单的状态转换:
// 状态接口
interface OrderState {
void next(Order order);
void previous(Order order);
void printStatus();
}
// 具体状态:已创建
class CreatedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new ShippedState());
}
@Override
public void previous(Order order) {
System.out.println("订单已创建,无法回退。");
}
@Override
public void printStatus() {
System.out.println("订单状态:已创建");
}
}
// 具体状态:已发货
class ShippedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new DeliveredState());
}
@Override
public void previous(Order order) {
order.setState(new CreatedState());
}
@Override
public void printStatus() {
System.out.println("订单状态:已发货");
}
}
// 具体状态:已送达
class DeliveredState implements OrderState {
@Override
public void next(Order order) {
System.out.println("订单已送达,无法继续推进。");
}
@Override
public void previous(Order order) {
order.setState(new ShippedState());
}
@Override
public void printStatus() {
System.out.println("订单状态:已送达");
}
}
// 上下文类:订单
class Order {
private OrderState state;
public Order() {
this.state = new CreatedState();
}
public void setState(OrderState state) {
this.state = state;
}
public void nextState() {
state.next(this);
}
public void previousState() {
state.previous(this);
}
public void printStatus() {
state.printStatus();
}
}
// 测试代码
public class StatePatternDemo {
public static void main(String[] args) {
Order order = new Order();
order.printStatus(); // 输出:订单状态:已创建
order.nextState();
order.printStatus(); // 输出:订单状态:已发货
order.nextState();
order.printStatus(); // 输出:订单状态:已送达
order.previousState();
order.printStatus(); // 输出:订单状态:已发货
}
}
总结
状态对象通过将状态和行为封装为独立的对象,实现了对象行为的动态变化。这种方式避免了复杂的条件分支,提高了代码的可维护性和扩展性。在实际开发中,状态对象常用于订单管理、工作流引擎等场景。
参数化基准测试
概念定义
参数化基准测试(Parametrized Benchmarking)是指在性能基准测试中,通过动态传入不同的参数值来测试同一段代码在不同输入条件下的性能表现。JMH(Java Microbenchmark Harness)提供了专门的机制来支持这种测试方式。
使用场景
- 不同输入规模的性能对比:例如测试排序算法在不同数据量下的表现
- 配置参数调优:测试不同线程池大小对性能的影响
- 边界条件测试:验证极端值或特殊输入时的性能表现
- 算法比较:用相同输入测试不同算法的性能差异
实现方式
JMH 主要通过 @Param
注解实现参数化测试:
@State(Scope.Benchmark)
public class MyBenchmark {
@Param({"10", "100", "1000"})
public int size;
private int[] data;
@Setup
public void setup() {
data = new int[size];
// 初始化数据
}
@Benchmark
public void testMethod(Blackhole bh) {
// 使用size参数的测试代码
bh.consume(Arrays.sort(data.clone()));
}
}
注意事项
- 状态管理:必须使用
@State
注解标记参数类 - 参数类型:
@Param
支持基本类型和字符串 - 参数组合:多个
@Param
会产生所有可能的参数组合 - 初始化成本:每个参数组合都会创建新的基准测试实例
- 结果分析:建议使用
-prof gc
分析不同参数下的内存行为
示例进阶用法
@State(Scope.Benchmark)
public class AdvancedParamBenchmark {
@Param({"LinkedList", "ArrayList"})
public String listType;
@Param({"100", "10000"})
public int elements;
private List<Integer> list;
@Setup
public void setup() {
switch(listType) {
case "LinkedList":
list = new LinkedList<>();
break;
case "ArrayList":
list = new ArrayList<>();
break;
}
// 填充数据
for(int i = 0; i < elements; i++) {
list.add(i);
}
}
@Benchmark
public void iterateList(Blackhole bh) {
for(Integer i : list) {
bh.consume(i);
}
}
}
结果解读
运行后会为每个参数组合生成独立的测试结果,例如:
Benchmark (listType) (elements) Mode Cnt Score Error
AdvancedParamBenchmark.iterate LinkedList 100 thrpt 5 45678.9 ± 1.5
AdvancedParamBenchmark.iterate LinkedList 10000 thrpt 5 1234.5 ± 0.7
AdvancedParamBenchmark.iterate ArrayList 100 thrpt 5 56789.1 ± 2.1
AdvancedParamBenchmark.iterate ArrayList 10000 thrpt 5 3456.7 ± 1.2
这种输出可以清晰展示不同参数组合下的性能差异。
死代码消除问题与解决
什么是死代码消除?
死代码消除(Dead Code Elimination,DCE)是编译器或即时编译器(JIT)的一种优化技术,它会自动识别并移除那些对程序最终结果没有影响的代码。这些代码被称为“死代码”,因为它们不会影响程序的输出或状态。
死代码消除在性能测试中的问题
在性能基准测试中,死代码消除可能导致测试结果不准确。例如,如果你正在测试一个方法的性能,但该方法的结果未被使用,编译器可能会将其优化掉,导致测试时间远低于实际运行时间。
常见的死代码消除场景
- 未使用的返回值:如果方法的返回值未被使用,编译器可能会认为该方法调用是多余的。
- 常量表达式:如果表达式的结果是常量,编译器可能会直接替换为结果。
- 循环中的不变代码:如果循环中的某些代码每次迭代结果相同,编译器可能会将其移出循环。
如何避免死代码消除?
在 JMH 中,可以通过以下方法避免死代码消除:
- 使用
@Benchmark
方法的返回值:确保返回值被使用,例如通过 JMH 的Blackhole
对象。 - 引入随机性:通过输入参数或随机值防止编译器优化。
- 使用
Blackhole
对象:JMH 提供的Blackhole
可以“消耗”返回值,避免优化。
示例代码
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class DeadCodeEliminationBenchmark {
private int x = 1;
private int y = 2;
// 错误示例:可能被优化掉
@Benchmark
public int testWithoutBlackhole() {
return x + y;
}
// 正确示例:使用 Blackhole 避免优化
@Benchmark
public void testWithBlackhole(Blackhole bh) {
bh.consume(x + y);
}
}
注意事项
- 避免过度优化:确保测试代码与实际生产代码的行为一致。
- 检查编译器日志:可以通过 JVM 参数(如
-XX:+PrintCompilation
)观察编译器优化行为。 - 多次验证结果:如果测试结果异常低,可能是死代码消除导致的。
通过以上方法,可以有效地避免死代码消除对性能测试结果的影响,确保测试的准确性。
编译器优化影响
概念定义
编译器优化影响是指在性能基准测试中,由于现代编译器(如JVM的JIT编译器)的优化行为,导致测试结果与实际运行时性能出现偏差的现象。这些优化可能包括死代码消除(Dead Code Elimination)、循环展开(Loop Unrolling)、常量折叠(Constant Folding)等。
常见优化类型
-
死代码消除(DCE)
编译器会移除对结果无影响的代码。例如:int result = 0; for (int i = 0; i < 1000; i++) { result += i; // 若result未被使用,整个循环可能被优化掉 }
-
循环展开(Loop Unrolling)
将循环体复制多次以减少循环控制开销:// 原始循环 for (int i = 0; i < 4; i++) { System.out.println(i); } // 可能被优化为 System.out.println(0); System.out.println(1); System.out.println(2); System.out.println(3);
-
内联(Inlining)
将小方法调用替换为方法体代码,减少调用开销。
JMH的应对机制
-
@Benchmark
方法返回值处理
通过Blackhole
对象消耗返回值,防止DCE:@Benchmark public void testMethod(Blackhole bh) { bh.consume(compute()); // 强制编译器保留计算 }
-
状态对象(
@State
)
使用可变状态避免常量折叠:@State(Scope.Thread) public static class MyState { public int value = ThreadLocalRandom.current().nextInt(); }
-
控制内联行为
通过-XX:CompileCommand
参数限制特定方法的内联。
典型误区
-
忽略预热阶段
未充分预热会导致测试JIT未优化的代码。JMH默认包含预热迭代。 -
测试数据过于简单
常量或规律数据易被优化,应使用随机性输入:@State(Scope.Thread) public static class Data { int[] values = new Random().ints(1000).toArray(); }
-
未考虑分层编译
JVM的C1/C2编译器在不同阶段应用不同优化,需足够长的测试时间。
验证优化影响的方法
- 添加
-XX:+PrintCompilation
查看JIT日志 - 使用
-prof perfasm
生成汇编代码(需Linux环境) - 对比
@Fork(0)
(禁用JIT)与正常测试的结果差异
示例代码对比
// 易被优化的写法
@Benchmark
public int poorBenchmark() {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
return sum; // 可能被完全优化为return 499500
}
// 正确写法
@Benchmark
public void goodBenchmark(Blackhole bh) {
int sum = 0;
for (int i = 0; i < 1000; i++) {
sum += i;
}
bh.consume(sum);
}
最佳实践
- 始终通过
Blackhole
消费计算结果 - 使用
@State
对象提供可变输入 - 避免在基准方法内创建大量临时对象
- 检查生成的汇编代码(
-prof perfasm
)确认关键路径未被优化
Blackhole 防止优化
概念定义
Blackhole(黑洞)是 JMH 性能基准测试中的一个关键工具类,用于防止 JVM 的即时编译器(JIT)对测试代码进行优化(如死代码消除)。它通过“吞噬”方法返回值或变量,强制 JVM 执行计算逻辑,确保基准测试结果的准确性。
使用场景
- 防止死代码消除:当测试方法的返回值未被使用时,JIT 可能跳过计算(如
return a + b
未被消费时直接优化掉)。 - 避免常量折叠:若输入是常量,JIT 可能直接预计算并替换结果(如
return 2 + 3
被替换为5
)。 - 控制变量消耗:确保中间变量的计算不被优化(如循环中的临时变量)。
常见误区
- 过度使用:非必要的
Blackhole
调用会增加额外开销。 - 错误传递:直接传递原始值而非计算后的值(如
blackhole.consume(a + b)
正确,而分开consume(a)
和consume(b)
可能无法阻止优化)。 - 忽略对象分配:未处理对象分配时的优化(需配合
@CompilerControl
注解)。
示例代码
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class BlackholeExample {
private int a = 2, b = 3;
// 错误示例:返回值可能被优化掉
@Benchmark
public int baseline() {
return a + b;
}
// 正确用法:通过 Blackhole 强制计算
@Benchmark
public void withBlackhole(Blackhole blackhole) {
blackhole.consume(a + b);
}
// 复杂场景:防止循环中的计算被优化
@Benchmark
public void loopWithBlackhole(Blackhole blackhole) {
for (int i = 0; i < 1000; i++) {
blackhole.consume(a * i + b);
}
}
}
注意事项
- JMH 自动注入:
Blackhole
参数由 JMH 运行时自动提供,无需手动实例化。 - 原始类型支持:
consume()
方法重载了所有基本类型(如int
、long
)和对象类型。 - 性能影响:
Blackhole
本身有纳秒级开销,需在测试结果中合理评估。
控制台输出与格式化
概念定义
控制台输出是指程序将信息显示在终端或命令行界面上的过程。在 Java 中,主要通过 System.out
和 System.err
两个标准流实现。格式化则是指按照特定格式(如对齐、精度、占位符等)输出数据,使其更易读或符合特定要求。
常用方法
-
System.out.println()
输出内容后自动换行,适用于简单调试信息。System.out.println("Hello, World!");
-
System.out.print()
输出内容不换行,需手动添加\n
或调用println()
换行。System.out.print("Hello, "); System.out.print("World!\n");
-
System.out.printf()
支持格式化字符串(类似 C 语言的printf
),通过占位符控制输出格式。String name = "Alice"; int age = 25; System.out.printf("Name: %s, Age: %d\n", name, age);
-
String.format()
返回格式化后的字符串而不直接输出,适合需要复用格式化结果的场景。String formatted = String.format("Value: %.2f", 3.14159); System.out.println(formatted); // 输出: Value: 3.14
格式化占位符
占位符 | 说明 | 示例 |
---|---|---|
%s | 字符串 | "%s" → "text" |
%d | 十进制整数 | "%d" → 42 |
%f | 浮点数(默认 6 位小数) | "%f" → 3.141593 |
%.2f | 浮点数(保留 2 位小数) | "%.2f" → 3.14 |
%n | 换行符(平台无关) | "Line1%nLine2" |
%t | 日期时间(需搭配子格式符) | "%tY" → 2023 |
高级格式化示例
-
对齐与宽度控制
%-10s
表示左对齐并占 10 字符宽度,%5d
表示右对齐占 5 字符宽度。System.out.printf("%-10s %5d\n", "Item1", 100); // 输出: Item1 100 System.out.printf("%-10s %5d\n", "LongItem", 9999); // 输出: LongItem 9999
-
数字格式化
千位分隔符、补零等。System.out.printf("%,d\n", 1000000); // 输出: 1,000,000 System.out.printf("%05d\n", 42); // 输出: 00042
-
日期格式化
使用%t
配合子格式符(如Y
年、m
月)。Date now = new Date(); System.out.printf("%tY-%tm-%td\n", now, now, now); // 输出: 2023-10-05
注意事项
-
性能影响
频繁调用System.out.println()
可能影响性能(尤其在循环中),生产环境建议使用日志框架(如 SLF4J)。 -
本地化差异
某些格式化符号(如千位分隔符)依赖系统区域设置,可能在不同环境下表现不同。 -
异常处理
printf
若格式字符串与参数不匹配会抛出IllegalFormatException
。
示例代码
public class FormatDemo {
public static void main(String[] args) {
// 基础格式化
System.out.printf("PI: %.3f, Hex: %x%n", Math.PI, 255);
// 表格对齐输出
String[] items = {"Apple", "Banana", "Cherry"};
int[] counts = {10, 5, 20};
System.out.println("------------------");
System.out.printf("%-10s %5s%n", "Item", "Count");
System.out.println("------------------");
for (int i = 0; i < items.length; i++) {
System.out.printf("%-10s %5d%n", items[i], counts[i]);
}
}
}
输出结果:
PI: 3.142, Hex: ff
------------------
Item Count
------------------
Apple 10
Banana 5
Cherry 20
生成 JSON/CSV 报告
概述
JMH 提供了生成 JSON 和 CSV 格式报告的功能,方便开发者对性能测试结果进行进一步分析或集成到其他工具中。这些报告包含了完整的基准测试结果,包括吞吐量、平均时间、误差等关键指标。
使用场景
- 持续集成:将测试结果集成到 CI/CD 流水线中,进行自动化分析。
- 数据分析:将结果导入 Excel、Tableau 等工具进行可视化分析。
- 长期跟踪:保存历史测试结果,对比性能变化趋势。
生成 JSON 报告
通过 -rf json
参数指定生成 JSON 格式报告:
java -jar target/benchmarks.jar -rf json
生成的 JSON 文件会包含以下关键信息:
- 基准测试名称
- 模式(吞吐量/平均时间等)
- 得分(ops/time)
- 误差范围
- 单位
- 参数(如果有)
生成 CSV 报告
通过 -rf csv
参数指定生成 CSV 格式报告:
java -jar target/benchmarks.jar -rf csv
CSV 文件通常包含以下列:
- Benchmark
- Mode
- Threads
- Samples
- Score
- Score Error
- Unit
- Param: (如果有参数化测试)
自定义输出路径
可以使用 -rff
参数指定报告文件路径:
java -jar target/benchmarks.jar -rf json -rff results.json
示例 JSON 报告片段
{
"jmhVersion": "1.21",
"benchmark": "MyBenchmark.testMethod",
"mode": "thrpt",
"threads": 1,
"forks": 1,
"jvm": "/path/to/jvm",
"metric": "ops/us",
"score": 1234.567,
"scoreError": 12.345,
"scoreConfidence": [1222.222, 1246.912],
"params": {
"size": "100"
}
}
示例 CSV 报告
"Benchmark","Mode","Threads","Samples","Score","Score Error","Unit","Param: size"
"MyBenchmark.testMethod","thrpt",1,10,1234.567,12.345,"ops/us",100
注意事项
- 确保输出目录有写入权限
- 多次运行会覆盖同名文件
- CSV 文件可以用 Excel 直接打开分析
- JSON 格式更适合程序化处理
- 报告生成会增加少量额外开销
进阶用法
可以通过编程方式获取结果并生成自定义报告:
Options opt = new OptionsBuilder()
.include(MyBenchmark.class.getSimpleName())
.resultFormat(ResultFormatType.JSON)
.result("results.json")
.build();
new Runner(opt).run();
六、JMH 实战技巧
避免常见的基准测试陷阱
在进行 JMH 性能基准测试时,开发者往往会遇到一些常见的陷阱,这些陷阱可能导致测试结果不准确或误导性结论。以下是一些关键陷阱及其规避方法。
1. 未进行预热(Warm-up)
- 问题:JVM 的即时编译器(JIT)会在运行时优化热点代码,未预热直接测试会导致初始阶段的性能数据偏低。
- 解决方案:使用 JMH 的
@Warmup
注解设置足够的预热迭代次数。@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS) public class MyBenchmark { // 测试代码 }
2. 忽略 JVM 优化(如死代码消除)
- 问题:如果测试代码的结果未被使用,JVM 可能会直接优化掉整个计算过程。
- 解决方案:通过 JMH 的
Blackhole
类强制消费计算结果。@Benchmark public void testMethod(Blackhole bh) { int result = compute(); bh.consume(result); // 避免死代码消除 }
3. 测试数据过于简单
- 问题:使用固定或极简数据可能导致 JVM 过度优化,无法反映真实场景。
- 解决方案:引入随机性或真实数据集。
@State(Scope.Thread) public class MyBenchmark { private int[] data; @Setup public void setup() { data = generateRealisticData(); // 模拟真实数据 } }
4. 未考虑多线程环境
- 问题:单线程测试无法暴露并发场景下的性能问题(如锁竞争)。
- 解决方案:使用
@Threads
注解模拟多线程。@Benchmark @Threads(4) // 模拟 4 个并发线程 public void concurrentTest() { // 并发测试代码 }
5. 忽略系统噪声(OS/GC 干扰)
- 问题:操作系统调度或垃圾回收会干扰测试结果。
- 解决方案:
- 增加测试迭代次数(通过
@Measurement
注解)。 - 在稳定的环境中运行(如关闭其他程序)。
- 使用
-prof gc
分析 GC 影响。
- 增加测试迭代次数(通过
6. 错误的时间单位选择
- 问题:纳秒级测试可能包含大量噪声,而秒级测试可能掩盖细节。
- 解决方案:根据实际场景选择合适的时间单位(如
TimeUnit.MICROSECONDS
)。
7. 未隔离测试状态
- 问题:多个测试方法共享状态可能导致交叉干扰。
- 解决方案:使用
@State
注解明确作用域。@State(Scope.Thread) // 每个线程独立状态 public class MyBenchmark { private int counter; }
8. 过早优化
- 问题:根据不稳定的基准测试结果进行优化。
- 解决方案:确保测试结果具有统计显著性(通过多次运行和方差分析)。
微基准测试注意事项
微基准测试(Microbenchmarking)是一种针对代码中极小部分(如单个方法或算法)进行性能测量的技术。虽然JMH(Java Microbenchmark Harness)等工具可以简化这一过程,但在实际应用中仍有许多需要注意的细节。以下是进行微基准测试时的关键注意事项:
1. 避免死代码消除(Dead Code Elimination)
JVM 的即时编译器(JIT)会优化掉未被使用的代码(即“死代码”),导致测试结果失真。
错误示例:
@Benchmark
public void testMethod() {
int result = compute(); // 计算结果未被使用,可能被JIT优化掉
}
正确做法:
使用 Blackhole
消费计算结果,防止优化:
@Benchmark
public void testMethod(Blackhole blackhole) {
int result = compute();
blackhole.consume(result); // 显式告知JVM需要计算结果
}
2. 预热(Warm-up)的重要性
JVM 需要时间达到稳定状态(如JIT编译、缓存预热)。未充分预热会导致测试结果不准确。
JMH 配置示例:
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS) // 预热5轮,每轮1秒
@Measurement(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) // 正式测试10轮
public class MyBenchmark { ... }
3. 避免循环优化(Loop Unrolling)
JIT 可能会展开循环(Loop Unrolling),导致循环体内的代码被多次重复执行,影响测试结果。
错误示例:
@Benchmark
public void loopTest() {
for (int i = 0; i < 1000; i++) {
compute(); // JIT可能展开循环,导致测试不准确
}
}
正确做法:
使用 @OperationsPerInvocation
或通过参数控制循环次数:
@Benchmark
@OperationsPerInvocation(1000) // 明确每次调用包含1000次操作
public void singleOperation() {
compute();
}
4. 注意环境干扰
- 后台进程:关闭不必要的程序,避免CPU、内存竞争。
- CPU 频率缩放:禁用动态调频(如Intel Turbo Boost),保持CPU频率稳定。
- 多线程干扰:确保测试环境是单线程或明确控制并发。
5. 避免常量折叠(Constant Folding)
JVM 可能会在编译时将常量表达式直接计算结果,导致测试失真。
错误示例:
@Benchmark
public int constantFold() {
return 10 + 20; // 编译时直接被替换为30
}
正确做法:
使用非常量输入(如从方法参数或字段中读取):
@State(Scope.Thread)
public class MyBenchmark {
private int a = 10;
private int b = 20;
@Benchmark
public int dynamicCompute() {
return a + b; // 避免常量折叠
}
}
6. 统计结果的正确解读
- 关注置信区间:JMH 默认提供平均值、标准差和置信区间(如99.9%)。
- 避免单一运行:多次运行取中位数或平均值,减少偶然性。
- 比较相对值:不同硬件环境下的绝对时间无意义,应关注相对性能差异。
7. 避免测试无关操作
确保测试代码仅包含目标逻辑,避免初始化、日志打印等无关操作。
错误示例:
@Benchmark
public void testWithLogging() {
Logger.info("Start"); // 日志输出会严重影响性能
compute();
}
8. 合理选择测试模式
JMH 支持多种模式(Mode.Throughput
、Mode.AverageTime
等),需根据场景选择:
- 吞吐量模式:
Mode.Throughput
(单位时间操作数)。 - 平均时间模式:
Mode.AverageTime
(单次操作耗时)。
9. 注意 JVM 的分层编译
- 混合模式:JVM 可能同时使用解释器和JIT编译器,导致结果波动。
- 解决方案:通过
-XX:+TieredCompilation
控制编译策略,或延长预热时间。
10. 避免测试代码与生产代码差异
- 数据规模:测试数据应与生产环境一致(如集合大小、字符串长度)。
- 依赖模拟:使用真实或接近真实的依赖(如数据库连接、网络延迟)。
多线程基准测试
概念定义
多线程基准测试是指通过模拟多线程并发环境,对程序在多线程条件下的性能表现进行测量和评估的过程。它可以帮助开发者发现并发环境下的性能瓶颈、线程安全问题以及资源竞争等问题。
使用场景
- 并发数据结构性能测试:如测试
ConcurrentHashMap
和HashMap
在多线程环境下的性能差异。 - 锁机制性能测试:比较不同锁(如
synchronized
、ReentrantLock
)在高并发场景下的性能表现。 - 线程池优化:测试不同线程池配置(如核心线程数、队列大小)对任务处理性能的影响。
- 并发算法验证:验证多线程算法(如并行排序、并行计算)的性能提升效果。
常见误区与注意事项
- 线程安全问题:基准测试代码本身必须是线程安全的,否则测试结果可能不准确。
- 预热问题:JVM 需要预热(Warm-up)才能达到稳定性能状态,直接测试冷启动性能可能不准确。
- 资源竞争:避免测试过程中因外部资源(如数据库、网络)成为瓶颈,影响测试结果。
- 线程数选择:线程数并非越多越好,需根据实际场景和硬件资源合理设置。
- 结果解读:注意区分吞吐量(Throughput)和延迟(Latency)指标。
示例代码(JMH 实现)
以下是一个使用 JMH 测试 ConcurrentHashMap
多线程性能的示例:
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 1)
@Threads(4) // 测试4个线程并发
@Fork(1)
public class ConcurrentHashMapBenchmark {
private ConcurrentHashMap<Integer, String> map;
@Setup
public void setup() {
map = new ConcurrentHashMap<>();
// 初始化1000个键值对
for (int i = 0; i < 1000; i++) {
map.put(i, "value_" + i);
}
}
@Benchmark
public String testGet() {
int key = (int) (Math.random() * 1000);
return map.get(key);
}
@Benchmark
public void testPut() {
int key = (int) (Math.random() * 1000);
map.put(key, "new_value");
}
}
关键参数说明
@Threads(4)
:指定并发线程数为4。@BenchmarkMode(Mode.Throughput)
:测试吞吐量(每秒操作数)。@Warmup
:预热3轮,每轮1秒。@Measurement
:正式测量5轮,每轮1秒。
结果分析
运行后会输出类似以下结果:
Benchmark Mode Cnt Score Error Units
ConcurrentHashMapBenchmark.testGet thrpt 5 123456.789 ± 9876.543 ops/s
ConcurrentHashMapBenchmark.testPut thrpt 5 98765.432 ± 1234.567 ops/s
Score
:表示每秒操作数(越高越好)。Error
:表示误差范围。
测试结果分析与解读
基本概念
JMH(Java Microbenchmark Harness)生成的测试结果通常包含多个维度的性能指标,如吞吐量(ops/ms)、平均执行时间(ms/op)、误差范围等。正确分析这些数据是性能优化的关键前提。
关键指标解析
吞吐量(Throughput)
- 定义:单位时间内完成的操作次数(ops/time)
- 示例值:
Score: 123456.789 ops/ms
- 解读:数值越高表示性能越好,适用于衡量系统处理能力
平均时间(Average Time)
- 定义:单次操作消耗的平均时间(time/op)
- 示例值:
Score: 0.008 ms/op
- 解读:数值越低性能越好,适合评估延迟敏感场景
百分位数(Percentiles)
Result "testMethod":
0.000 ±(99.9%) 0.001 ms/op [Average]
(min, avg, max) = (0.007, 0.008, 0.012), stdev = 0.001
CI (99.9%): [0.007, 0.009] (assumes normal distribution)
常见分析维度
-
稳定性分析
- 检查误差范围(±符号后的值)
- 标准差(stdev)应小于平均值的10%
- 示例问题:
±50%
的误差说明测试结果不可靠
-
对比分析
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
public class MyBenchmark {
@Benchmark
public void testVersion1() { /*...*/ }
@Benchmark
public void testVersion2() { /*...*/ }
}
- 趋势分析
- 关注(min, avg, max)三值关系
- 理想情况:三者接近且max不超过avg的2倍
常见误区
-
忽略JVM预热
- 错误做法:直接使用第一个迭代周期的数据
- 正确做法:分析
@Warmup
阶段后的稳定数据
-
过度关注单次结果
- 必须结合多次运行(通常3-5次)的聚合结果
-
环境干扰
- CPU频率波动
- 后台进程影响
- 解决方案:使用
-jvmArgs="-XX:+LockExperimentalVMOptions -XX:+UseEpsilonGC"
高级分析技巧
性能瓶颈定位
Hottest code regions:
50.0% org.sample.MyClass.method1
30.0% java.util.HashMap.getNode
20.0% other_methods
JIT编译影响
- 检查
-prof perfasm
输出的汇编代码 - 关注
@CompilerControl
注解的使用
可视化工具
- JMH Visualizer(在线工具)
- JFR(Java Flight Recorder)集成
java -jar benchmarks.jar -prof jfr
典型问题模式
- 性能回退:新版本比旧版本慢20%以上
- 性能抖动:相同输入产生±15%以上的波动
- 异常值:个别操作耗时超过平均值的10倍
报告建议格式
## 结论摘要
- 最优方案:AlgorithmB(比基线快42%)
- 关键发现:HashMap初始容量影响显著
## 详细数据
| 方案 | 吞吐量(ops/ms) | 误差范围 |
|------------|----------------|----------|
| Baseline | 10,000 | ±2% |
| AlgorithmA | 12,500 | ±3% |
| AlgorithmB | 14,200 | ±1.5% |
## 优化建议
1. 使用预先初始化的HashMap(大小1024)
2. 避免在循环内创建临时对象
性能对比测试案例
概念定义
性能对比测试案例是指使用 JMH(Java Microbenchmark Harness)框架编写的代码片段,用于比较不同算法、数据结构或实现方式在特定场景下的性能表现。这些案例通常包含多个可对比的实现,并通过 JMH 的基准测试功能生成准确的性能数据(如吞吐量、平均执行时间等)。
使用场景
- 算法优化:比较不同算法(如快速排序 vs 归并排序)在相同数据集上的性能。
- API 选择:对比不同库或 JDK 版本中相同功能的性能差异(如
String.concat
vsStringBuilder
)。 - 配置调优:测试同一功能在不同参数配置下的表现(如线程池大小对并发性能的影响)。
常见误区与注意事项
- 避免 JVM 优化干扰:
- 使用
@State
注解管理测试数据,防止常量折叠(Constant Folding)。 - 通过
Blackhole
消费计算结果,避免死代码消除(Dead Code Elimination)。
- 使用
- 预热的重要性:
- 添加
@Warmup
注解确保 JIT 编译完成后再采集数据。
- 添加
- 统计显著性:
- 设置足够的
@Measurement
迭代次数,避免偶然性结果。
- 设置足够的
示例代码
以下是一个对比 ArrayList
和 LinkedList
遍历性能的测试案例:
import org.openjdk.jmh.annotations.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class ListIterationBenchmark {
@Param({"1000", "10000"})
private int size;
private List<Integer> arrayList;
private List<Integer> linkedList;
@Setup
public void setup() {
arrayList = new ArrayList<>(size);
linkedList = new LinkedList<>();
for (int i = 0; i < size; i++) {
arrayList.add(i);
linkedList.add(i);
}
}
@Benchmark
public int testArrayList(Blackhole bh) {
int sum = 0;
for (Integer num : arrayList) {
sum += num;
}
bh.consume(sum);
return sum;
}
@Benchmark
public int testLinkedList(Blackhole bh) {
int sum = 0;
for (Integer num : linkedList) {
sum += num;
}
bh.consume(sum);
return sum;
}
}
关键注解说明
注解 | 作用 |
---|---|
@State | 定义测试数据的生命周期 |
@Param | 参数化测试(不同数据规模) |
@Benchmark | 标记待测试的方法 |
Blackhole | 防止 JVM 优化掉未使用的结果 |
七、JMH 集成与扩展
与 JUnit 集成
概念定义
JMH(Java Microbenchmark Harness)与 JUnit 集成是指将 JMH 的微基准测试功能嵌入到 JUnit 测试框架中,从而能够在常规的单元测试环境中运行性能基准测试。这种集成允许开发者在同一个项目中同时编写功能测试和性能测试,便于持续集成(CI)和自动化测试流程。
使用场景
- 持续集成环境:在 CI/CD 流水线中,既运行功能测试,又运行性能基准测试,确保代码的性能不会因变更而退化。
- 开发阶段:开发者在编写功能代码时,可以方便地运行性能测试,快速验证优化效果。
- 性能监控:通过定期运行 JMH 测试,监控关键代码路径的性能变化。
常见误区或注意事项
- 测试环境干扰:JUnit 测试通常运行在共享的测试环境中,可能受到其他测试或后台进程的影响,导致 JMH 测试结果不准确。
- 解决方案:确保 JMH 测试运行在独立的环境中,或通过配置
@Fork
注解隔离测试进程。
- 解决方案:确保 JMH 测试运行在独立的环境中,或通过配置
- 测试时间过长:JMH 测试通常需要多次迭代以获取稳定结果,可能导致测试套件运行时间过长。
- 解决方案:在 CI 中分离性能测试和功能测试,或限制 JMH 的迭代次数。
- 误用 JUnit 断言:JMH 测试的目的是测量性能,而非验证功能,避免在基准测试中使用 JUnit 的断言。
- 正确做法:通过 JMH 的
@Benchmark
方法返回结果,由 JMH 框架处理。
- 正确做法:通过 JMH 的
示例代码
以下是一个将 JMH 与 JUnit 集成的示例:
1. 添加依赖
在 pom.xml
中添加 JMH 和 JUnit 的依赖:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.36</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.36</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
2. 编写 JMH 基准测试
通过 JUnit 的 @Test
注解触发 JMH 测试:
import org.junit.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
public class JMHWithJUnitTest {
@Benchmark
public void testMethod() {
// 被测代码逻辑
Math.log(123.456);
}
@Test
public void runBenchmarks() throws Exception {
Options opt = new OptionsBuilder()
.include(this.getClass().getName() + ".*")
.build();
new Runner(opt).run();
}
}
3. 运行测试
直接运行 JUnit 测试方法 runBenchmarks()
,JMH 会自动执行基准测试并输出结果。
替代方案:使用 jmh-junit-benchmark
如果需要更紧密的集成,可以使用 jmh-junit-benchmark
库:
- 添加依赖:
<dependency>
<groupId>com.carrotsearch</groupId>
<artifactId>jmh-junit-benchmark</artifactId>
<version>0.2.0</version>
</dependency>
- 编写测试:
import com.carrotsearch.junitbenchmarks.BenchmarkOptions;
import com.carrotsearch.junitbenchmarks.BenchmarkRule;
import org.junit.Rule;
import org.junit.Test;
public class JUnitBenchmarkTest {
@Rule
public BenchmarkRule benchmarkRule = new BenchmarkRule();
@Test
@BenchmarkOptions(benchmarkRounds = 20, warmupRounds = 5)
public void benchmarkMethod() {
// 被测代码逻辑
}
}
JMH 与 CI/CD 集成
什么是 CI/CD 集成
CI/CD(持续集成/持续交付)是一种软件开发实践,通过自动化流程快速、频繁地构建、测试和部署代码。将 JMH(Java Microbenchmark Harness)集成到 CI/CD 流水线中,可以在每次代码变更后自动运行性能基准测试,确保代码的性能不会因修改而退化。
为什么需要集成 JMH 到 CI/CD
- 性能回归检测:在代码提交或合并时自动运行基准测试,及时发现性能退化问题。
- 持续监控:长期跟踪关键代码路径的性能变化,形成历史趋势。
- 开发反馈:为开发者提供即时性能反馈,促进性能优化文化。
集成方式
1. 作为测试阶段的一部分
将 JMH 基准测试作为 Maven/Gradle 构建的一部分,在 verify
或 test
阶段执行:
<!-- Maven 示例 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<includes>
<include>**/*Benchmark.java</include>
</includes>
</configuration>
</plugin>
2. 专用性能测试阶段
在 CI 流水线中创建独立的性能测试阶段,与其他测试阶段分离:
# GitLab CI 示例
performance_test:
stage: performance
script:
- mvn clean verify -DskipTests -DskipITs
- java -jar target/benchmarks.jar
artifacts:
paths:
- jmh-result.json
常见 CI/CD 工具集成
Jenkins
使用 Performance Plugin 收集和分析 JMH 结果:
pipeline {
stages {
stage('Performance Test') {
steps {
sh 'java -jar benchmarks.jar -rf json -rff jmh-result.json'
perfReport 'jmh-result.json'
}
}
}
}
GitHub Actions
通过 action 运行和可视化 JMH 结果:
- name: Run JMH
run: java -jar target/benchmarks.jar -rf json -rff jmh-result.json
- name: Upload results
uses: actions/upload-artifact@v2
with:
name: jmh-results
path: jmh-result.json
最佳实践
- 选择关键测试:只运行对业务影响最大的基准测试,控制执行时间。
- 设置阈值:定义性能退化阈值(如 ±5%),超过阈值时使构建失败。
- 环境一致性:确保 CI 环境稳定(CPU 隔离、无资源竞争)。
- 结果存储:将历史结果存入数据库(如 InfluxDB)进行趋势分析。
- 可视化:通过 Grafana 等工具展示性能变化趋势。
示例:Gradle 集成
jmh {
includeTests = false
resultFormat = 'JSON'
resultsFile = file('build/jmh-results.json')
warmupIterations = 3
iterations = 5
}
task performanceTest(type: Test) {
include '**/*Benchmark*'
outputs.file jmh.resultsFile
}
注意事项
- 避免生产环境运行:基准测试可能消耗大量资源,应在专用环境执行。
- 区分短期/长期测试:日常 CI 运行快速测试,夜间执行全面测试。
- 基准测试稳定性:多次运行取中位数,避免偶发波动影响判断。
- 资源监控:记录测试时的 CPU、内存等指标,辅助分析结果波动原因。
自定义 JMH 选项
概念定义
JMH(Java Microbenchmark Harness)是一个专门用于 Java 微基准测试的工具。自定义 JMH 选项允许开发者通过配置参数来调整基准测试的行为,以满足特定的测试需求。这些选项可以控制测试的迭代次数、预热次数、线程数、测试模式等。
使用场景
- 调整测试精度:通过增加迭代次数或预热次数,提高测试结果的准确性。
- 模拟多线程环境:通过设置线程数,测试代码在多线程环境下的性能表现。
- 优化测试时间:减少迭代次数或预热次数,快速获取初步测试结果。
- 特定测试模式:选择不同的测试模式(如吞吐量、平均时间、采样时间等)来满足不同的测试需求。
常见选项
以下是一些常用的 JMH 选项及其说明:
-
@Warmup
:配置预热阶段。iterations
:预热迭代次数。time
:每次预热迭代的时间。timeUnit
:时间单位(如TimeUnit.SECONDS
)。
-
@Measurement
:配置测量阶段。iterations
:测量迭代次数。time
:每次测量迭代的时间。timeUnit
:时间单位。
-
@Fork
:配置测试的进程数。value
:进程数。warmups
:预热进程数。
-
@Threads
:配置测试线程数。value
:线程数。
-
@BenchmarkMode
:配置测试模式。Mode.Throughput
:吞吐量模式(单位时间内的操作数)。Mode.AverageTime
:平均时间模式(每次操作的平均时间)。Mode.SampleTime
:采样时间模式(操作时间的分布)。
-
@OutputTimeUnit
:配置输出时间单位。TimeUnit.SECONDS
:秒。TimeUnit.MILLISECONDS
:毫秒。TimeUnit.MICROSECONDS
:微秒。
示例代码
以下是一个自定义 JMH 选项的示例代码:
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(value = 2, warmups = 1)
@Threads(4)
public class MyBenchmark {
@Benchmark
public void testMethod() {
// 测试代码
}
}
常见误区或注意事项
- 预热不足:预热次数或时间不足可能导致测试结果不准确。建议至少进行 3-5 次预热。
- 线程数设置不当:线程数过多可能导致资源竞争,过少则无法模拟真实场景。需根据实际需求调整。
- 测试模式选择错误:不同的测试模式适用于不同的场景。例如,吞吐量模式适合高并发场景,平均时间模式适合单次操作耗时测试。
- 忽略进程数:
@Fork
选项可以避免 JVM 优化对测试结果的影响。建议至少设置为 2。 - 时间单位混淆:确保
@OutputTimeUnit
和@Warmup
/@Measurement
中的时间单位一致,避免混淆。
通过合理配置这些选项,可以更准确地评估代码的性能表现。
扩展 JMH 功能
自定义 Benchmark 模式
JMH 默认提供 Throughput
、AverageTime
、SampleTime
等基准模式,但可通过实现 org.openjdk.jmh.runner.BenchmarkMode
注解扩展自定义模式:
@BenchmarkMode(MyCustomMode.class)
public class MyBenchmark {
// 基准方法
}
自定义状态管理
JMH 的 @State
注解支持线程隔离和共享状态,可通过继承 org.openjdk.jmh.annotations.Scope
实现自定义作用域:
public class ClusterScope extends Scope {
@Override public boolean isThread() { return false; }
@Override public boolean isGroup() { return true; }
}
@State(ClusterScope.class)
public class MyState {
// 集群级共享状态
}
自定义 Profiler 集成
通过实现 org.openjdk.jmh.profile.Profiler
接口集成第三方性能分析工具:
public class MyProfiler implements Profiler {
@Override public void beforeIteration(BenchmarkParams params, IterationParams iteration) {
// 启动分析
}
@Override public Collection<? extends Result> afterIteration(
BenchmarkParams params, IterationParams iteration, IterationResult result) {
// 收集分析结果
}
}
自定义结果格式输出
继承 org.openjdk.jmh.results.format.ResultFormat
实现自定义报告格式:
public class JsonResultFormat implements ResultFormat {
@Override public void writeOut(PrintWriter out, Collection<RunResult> results) {
// 生成JSON格式报告
}
}
注解处理器扩展
通过实现 org.openjdk.jmh.generators.core.BenchmarkProcessor
在编译期处理自定义注解:
@SupportedAnnotationTypes("com.my.Benchmark")
public class MyProcessor extends BenchmarkProcessor {
@Override protected void process(ProcessorContext context, RoundEnvironment roundEnv) {
// 处理自定义注解逻辑
}
}
自定义 Fixture 控制
通过 @Setup
/@TearDown
的 Level
扩展实现特殊生命周期控制:
@Benchmark
public void test(MyState state) {
// 基准代码
}
@State(Scope.Thread)
public static class MyState {
@Setup(Level.Trial)
public void init() {
// 整个测试执行前初始化
}
}
参数化测试增强
结合 @Param
注解与自定义参数生成器:
@Param({"1", "10", "100"})
public int size;
// 或使用自定义Provider
@Param(source = MyParamsProvider.class)
public String data;
异步基准测试支持
通过 Blackhole.consumeCPU
模拟异步操作:
@Benchmark
public void asyncOp(Blackhole bh) {
CompletableFuture.runAsync(() -> {
bh.consumeCPU(1000); // 模拟异步耗时操作
});
}
自定义 Warmup 策略
实现 org.openjdk.jmh.runner.parameters.WarmupMode
控制预热行为:
@Warmup(iterations = 3, time = 1, timeUnit = TimeUnit.SECONDS,
mode = WarmupMode.INDI)
public class MyBenchmark {
// 独立模式预热
}
分布式测试支持
通过扩展 org.openjdk.jmh.runner.Runner
实现多机协同测试:
public class DistributedRunner extends Runner {
@Override public List<BenchmarkListEntry> list() throws IOException {
// 获取分布式节点测试列表
}
@Override public void run() throws IOException {
// 协调分布式执行
}
}
JMH 插件生态系统
概念定义
JMH(Java Microbenchmark Harness)插件生态系统是指围绕 JMH 框架的一系列扩展工具和插件,用于增强 JMH 的功能或简化其使用流程。这些插件通常由社区或第三方开发者开发,旨在解决 JMH 在特定场景下的局限性或提供额外的便利功能。
使用场景
- IDE 集成:如 IntelliJ IDEA 的 JMH 插件,可以直接在 IDE 中运行和调试 JMH 基准测试。
- 构建工具支持:如 Maven 或 Gradle 插件,用于自动化 JMH 测试的编译、运行和结果分析。
- 报告生成:插件可以生成更直观的 HTML 或可视化报告,便于分析性能数据。
- 代码生成:自动生成 JMH 测试模板代码,减少手动编写样板代码的工作量。
- 性能分析集成:与 Profiler 工具(如 Async Profiler)结合,提供更深入的性能分析。
常见插件示例
1. JMH IntelliJ Plugin
- 功能:在 IntelliJ IDEA 中直接运行 JMH 基准测试,支持断点调试和结果可视化。
- 安装方式:通过 IntelliJ 的插件市场搜索 “JMH” 安装。
- 示例配置:
@Benchmark @Fork(value = 1, warmups = 1) @BenchmarkMode(Mode.AverageTime) public void testMethod() { // 基准测试代码 }
2. JMH Gradle Plugin
- 功能:通过 Gradle 任务运行 JMH 测试,支持自定义配置和结果输出。
- 配置示例(
build.gradle
):plugins { id 'me.champeau.jmh' version '0.6.8' } jmh { include = ['com.example.*'] // 指定测试类 warmupIterations = 5 iterations = 10 }
3. JMH Maven Plugin
- 功能:通过 Maven 生命周期管理 JMH 测试,支持生成可执行 JAR。
- 配置示例(
pom.xml
):<plugin> <groupId>org.openjdk.jmh</groupId> <artifactId>jmh-maven-plugin</artifactId> <version>1.36</version> <executions> <execution> <goals> <goal>benchmark</goal> </goals> </execution> </executions> </plugin>
注意事项
- 版本兼容性:插件版本需与 JMH 核心库版本匹配,避免因版本冲突导致功能异常。
- 性能开销:部分插件(如 IDE 插件)可能引入额外开销,影响基准测试结果的准确性。
- 配置覆盖:插件可能覆盖 JMH 注解中的配置(如
@Fork
),需检查最终生效的配置。 - 插件维护:社区插件的维护状态参差不齐,建议选择活跃度高的插件。
示例:使用 JMH Gradle 插件运行测试
- 在
build.gradle
中配置插件:plugins { id 'java' id 'me.champeau.jmh' version '0.6.8' }
- 运行基准测试:
./gradlew jmh
- 查看结果:默认输出到
build/reports/jmh/results.txt
。