文章目录
- 1、JMH
- 2、运行方式二
- 3、死代码与黑洞变量
- 4、可视化分析
- 5、案例:日期格式化方法性能测试
- 6、总结
- 7、整合到SpringBoot
判断一个方法的耗时 ⇒ endTime-startTime ⇒ 不准确,首先部分对象懒加载,第一次请求会慢一些,其次,程序运行时,JIT即时编译器会实时优化代码,如随着执行次数的增加,程序性能逐渐优化:
⇒ JMH(Java Microbenchmark Harness)准确的测方法执行的性能
1、JMH
- https://github.com/openjdk/jmh
- JMH会先执行预热,确保JIT队代码优化之后再进行测试
- JMH项目搭建,新建个空目录,执行mvn生成模块
mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0
- 写一个简单案例
//执行5轮预热,每次持续1秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
//启动多少个进程 + 追加JMV参数
@Fork(value = 1, jvmArgsAppend = {"-Xms1g", "-Xmx1g"})
//指定显示平均时间,单位纳秒。Mode.ALL显示所有数据
@BenchmarkMode(Mode.AverageTime)
//纳秒
@OutputTimeUnit(TimeUnit.NANOSECONDS)
//变量共享的范围,对象在单个线程中共享就选Scope.Thread
@State(Scope.Benchmark)
public class MyBenchmark {@Benchmarkpublic int test1() {int i = 0;i++;return i;}}
- mvn verfiy校验并生成jar包
- 运行服务,得到结果
2、运行方式二
每次打jar包运行很麻烦,改用一个main方法:
//执行5轮预热,每次持续1秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
//启动多少个进程 + 追加JMV参数
@Fork(value = 1, jvmArgsAppend = {"-Xms1g", "-Xmx1g"})
//指定显示平均时间,单位纳秒。Mode.ALL显示所有数据
@BenchmarkMode(Mode.AverageTime)
//纳秒
@OutputTimeUnit(TimeUnit.NANOSECONDS)
//变量共享的范围,对象在单个线程中共享就选Scope.Thread
@State(Scope.Benchmark)
public class MyBenchmark {@Benchmarkpublic int test1() {int i = 0;i++;return i;}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder()//测哪个类.include(MyBenchmark.class.getSimpleName())//结果的格式,json文件//.resultFormat(ResultFormatType.JSON)//进程数.forks(1).build();new Runner(opt).run();}
}
结果相比jar包运行的方式略有出入,因为IDEA启动时自身会设置一些参数(所以要准确就建议用jar的方式)
3、死代码与黑洞变量
如下,定义的变量i,最后没有返回,JIT会认为i没有使用,会把这一段代码自动忽略掉
//...
public void testMethod() {int i = 0;i++;}
//...
此时,JMH返回的结果明显下降很多:
以上即死代码,JMH测试过程中,可用一个黑洞的变量,调用consume消费方法,就可以避免JIT把这些变量自动删除掉:
public void testMethod(Blackhole blackhole) {int i= 0;i++;int j = 0;j++;//使用黑洞,避免i,j变量没有使用被JIT当成死代码优化时去掉blackhole.consume(i);blackhole.consume(j);
}
4、可视化分析
- 生成json格式
//....resultFormat(ResultFormatType.JSON)
- 上传到:https://jmh.morethan.io/
5、案例:日期格式化方法性能测试
package org.sample;import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.results.format.ResultFormatType;
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 java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.concurrent.TimeUnit;//执行5轮预热,每次持续1秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
//执行一次测试
@Fork(value = 1, jvmArgsAppend = {"-Xms1g", "-Xmx1g"})
//显示平均时间,单位纳秒
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Thread)
public class DateBench {private static String sDateFormatString = "yyyy-MM-dd HH:mm:ss";private Date date = new Date();private LocalDateTime localDateTime = LocalDateTime.now();//优化private static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal();private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");@Setuppublic void setUp() {SimpleDateFormat sdf = new SimpleDateFormat(sDateFormatString);simpleDateFormatThreadLocal.set(sdf);}//测试Date@Benchmarkpublic String date() {SimpleDateFormat simpleDateFormat = new SimpleDateFormat(sDateFormatString);return simpleDateFormat.format(date);}//测试LocalDateTime@Benchmarkpublic String localDateTimeNotSave() {return localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));}/*** 优化测试Date的方法* SimpleDateFormat线程不安全,用ThreadLocal优化* 避免每次执行都创建一个SimpleDateFormat对象*/@Benchmarkpublic String dateThreadLocal() {return simpleDateFormatThreadLocal.get().format(date);}/*** 优化LocalDateTime*/@Benchmarkpublic String localDateTime() {return localDateTime.format(formatter);}public static void main(String[] args) throws RunnerException {Options opt = new OptionsBuilder().include(DateBench.class.getSimpleName()).resultFormat(ResultFormatType.JSON).forks(1).build();new Runner(opt).run();}
}
结果:
结论:
- Date对象使用的SimpleDateFormat线程不安全,每次需要重新创建对象,或者将对象放入ThreadLocal,后者性能好一点
- LocalDateTime对象使用的DateTimeFormatter线程安全,且性能好,将DateTimeFormatter对象保存一下,别每次都创建,性能会更好
6、总结
Jmh可以完全模拟运行环境中的Java虚拟机参数,同时支持预热能通过JIT执行优化后的代码获得更为准确的数据
7、整合到SpringBoot
<dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-core</artifactId><version>${jmh.version}</version><scope>test</scope>
</dependency><dependency><groupId>org.openjdk.jmh</groupId><artifactId>jmh-generator-annprocess</artifactId><version>${jmh.version}</version><scope>test</scope>
</dependency>
和之前单独一个jmh项目相比,整合时,可在UT中完成,setup初始是从IOC中获取要测试的方法所在类的Bean。且每次执行Benchmark注解的方法,都会运行一个新的SpringBoot服务,下面同时测5个方法,因此需要在配置文件中将端口设置成随机生成的。
import org.junit.jupiter.api.Test;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;
import org.openjdk.jmh.results.format.ResultFormatType;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;import java.io.IOException;
import java.util.concurrent.TimeUnit;//执行5轮预热,每次持续1秒
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
//执行一次测试
@Fork(value = 1, jvmArgsAppend = {"-Xms1g", "-Xmx1g"})
//显示平均时间,单位纳秒
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class PracticeBenchmarkTest {private UserController userController;private ApplicationContext context;//初始化将springboot容器启动 端口号随机@Setuppublic void setup() {this.context = new SpringApplication(JvmOptimizeApplication.class).run();userController = this.context.getBean(UserController.class);}//启动这个测试用例进行测试@Testpublic void executeJmhRunner() throws RunnerException, IOException {new Runner(new OptionsBuilder().shouldDoGC(true).forks(0).resultFormat(ResultFormatType.JSON).shouldFailOnError(true).build()).run();}//用黑洞消费数据,避免JIT消除代码@Benchmarkpublic void test1(final Blackhole bh) {bh.consume(userController.user1());}@Benchmarkpublic void test2(final Blackhole bh) {bh.consume(userController.user2());}@Benchmarkpublic void test3(final Blackhole bh) {bh.consume(userController.user3());}@Benchmarkpublic void test4(final Blackhole bh) {bh.consume(userController.user4());}@Benchmarkpublic void test5(final Blackhole bh) {bh.consume(userController.user5());}
}
随机端口:
server:
# port: 8882port: ${random.int(2000,8000)} #!!!!tomcat:threads:min-spare: 50max: 500