别再写 main 方法测试了,太 Low!这才是专业 Java 测试方法!

全栈架构社区

共 8832字,需浏览 18分钟

 · 2022-02-20

上一篇:还在用策略模式解决 if-else?Map+函数式接口方法才是YYDS!

作者:Richard_Yi

来源juejin.cn/post/6844903936869007368
前言

"If you cannot measure it, you cannot improve it".

在日常开发中,我们对一些代码的调用或者工具的使用会存在多种选择方式,在不确定他们性能的时候,我们首先想要做的就是去测量它。大多数时候,我们会简单的采用多次计数的方式来测量,来看这个方法的总耗时。

但是,如果熟悉JVM类加载机制的话,应该知道JVM默认的执行模式是JIT编译与解释混合执行。JVM通过热点代码统计分析,识别高频方法的调用、循环体、公共模块等,基于JIT动态编译技术,会将热点代码转换成机器码,直接交给CPU执行。



也就是说,JVM会不断的进行编译优化,这就使得很难确定重复多少次才能得到一个稳定的测试结果?所以,很多有经验的同学会在测试代码前写一段预热的逻辑。

JMH,全称 Java Microbenchmark Harness (微基准测试框架),是专门用于Java代码微基准测试的一套测试工具API,是由 OpenJDK/Oracle 官方发布的工具。何谓 Micro Benchmark 呢?简单地说就是在 method 层面上的 benchmark,精度可以精确到微秒级。

Java的基准测试需要注意的几个点:



JMH的使用场景:


  1. 定量分析某个热点函数的优化效果
  2. 想定量地知道某个函数需要执行多长时间,以及执行时间和输入变量的相关性
  3. 对比一个函数的多种实现方式


DEMO 演示


这里先演示一个DEMO,让不了解JMH的同学能够快速掌握这个工具的大概用法。


1. 测试项目构建


JMH是内置Java9及之后的版本。这里是以Java8进行说明。

为了方便,这里直接介绍使用maven构建JMH测试项目的方式。

第一种是使用命令行构建,在指定目录下执行以下命令:

$ mvn archetype:generate \          -DinteractiveMode=false \          -DarchetypeGroupId=org.openjdk.jmh \          -DarchetypeArtifactId=jmh-java-benchmark-archetype \          -DgroupId=org.sample \          -DartifactId=test \          -Dversion=1.0


对应目录下会出现一个test项目,打开项目后我们会看到这样的项目结构。



第二种方式就是直接在现有的maven项目中添加jmh-core和jmh-generator-annprocess的依赖来集成JMH。
            org.openjdk.jmh            jmh-core            ${jmh.version}                            org.openjdk.jmh            jmh-generator-annprocess            ${jmh.version}            provided        


2. 编写性能测试


这里我以测试LinkedList 通过index 方式迭代和foreach 方式迭代的性能差距为例子,编写测试类,涉及到的注解在之后会讲解,


/** * @author Richard_yyf * @version 1.0 2019/8/27 */

@State(Scope.Benchmark)@OutputTimeUnit(TimeUnit.SECONDS)@Threads(Threads.MAX)public class LinkedListIterationBenchMark {private static final int SIZE = 10000;

private List list = new LinkedList<>();
@Setup public void setUp() { for (int i = 0; i < SIZE; i++) { list.add(String.valueOf(i)); } }

@Benchmark @BenchmarkMode(Mode.Throughput) public void forIndexIterate() { for (int i = 0; i < list.size(); i++) { list.get(i); System.out.print(""); } }

@Benchmark @BenchmarkMode(Mode.Throughput) public void forEachIterate() { for (String s : list) { System.out.print(""); } }}


3. 执行测试


运行 JMH 基准测试有两种方式,一个是生产jar文件运行,另一个是直接写main函数或者放在单元测试中执行。

生成jar文件的形式主要是针对一些比较大的测试,可能对机器性能或者真实环境模拟有一些需求,需要将测试方法写好了放在linux环境执行。具体命令如下

$ mvn clean install$ java -jar target/benchmarks.jar


我们日常中遇到的一般是一些小测试,比如我上面写的例子,直接在IDE中跑就好了。启动方式如下:

 public static void main(String[] args) throws RunnerException {        Options opt = new OptionsBuilder()                .include(LinkedListIterationBenchMark.class.getSimpleName())                .forks(1)                .warmupIterations(2)                .measurementIterations(2)            .output("E:/Benchmark.log")                .build();

new Runner(opt).run(); }


4. 报告结果


输出结果如下


最后的结果:


Benchmark                                      Mode  Cnt     Score   Error  UnitsLinkedListIterationBenchMark.forEachIterate   thrpt    2  1192.380          ops/sLinkedListIterationBenchMark.forIndexIterate  thrpt    2   206.866          ops/s


整个过程:


# Detecting actual CPU count: 12 detected# JMH version: 1.21# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8# Warmup: 2 iterations, 10 s each# Measurement: 2 iterations, 10 s each# Timeout: 10 min per iteration# Threads: 12 threads, will synchronize iterations# Benchmark mode: Throughput, ops/time# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forEachIterate

# Run progress: 0.00% complete, ETA 00:01:20# Fork: 1 of 1# Warmup Iteration 1: 1189.267 ops/s# Warmup Iteration 2: 1197.321 ops/sIteration 1: 1193.062 ops/sIteration 2: 1191.698 ops/s



Result "org.sample.jmh.LinkedListIterationBenchMark.forEachIterate": 1192.380 ops/s



# JMH version: 1.21# VM version: JDK 1.8.0_131, Java HotSpot(TM) 64-Bit Server VM, 25.131-b11# VM invoker: C:\Program Files\Java\jdk1.8.0_131\jre\bin\java.exe# VM options: -javaagent:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\lib\idea_rt.jar=65175:D:\Program Files\JetBrains\IntelliJ IDEA 2018.2.2\bin -Dfile.encoding=UTF-8# Warmup: 2 iterations, 10 s each# Measurement: 2 iterations, 10 s each# Timeout: 10 min per iteration# Threads: 12 threads, will synchronize iterations# Benchmark mode: Throughput, ops/time# Benchmark: org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate

# Run progress: 50.00% complete, ETA 00:00:40# Fork: 1 of 1# Warmup Iteration 1: 205.676 ops/s# Warmup Iteration 2: 206.512 ops/sIteration 1: 206.542 ops/sIteration 2: 207.189 ops/s

Result "org.sample.jmh.LinkedListIterationBenchMark.forIndexIterate": 206.866 ops/s

# Run complete. Total time: 00:01:21

REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up onwhy the numbers are the way they are. Use profilers (see -prof, -lprof), design factorialexperiments, perform baseline and negative tests that provide experimental control, make surethe benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.Do not assume the numbers tell you what you want them to tell.

Benchmark Mode Cnt Score Error UnitsLinkedListIterationBenchMark.forEachIterate thrpt 2 1192.380 ops/sLinkedListIterationBenchMark.forIndexIterate  thrpt    2   206.866          ops/s


注解介绍


下面我们来详细介绍一下相关的注解


@BenchmarkMode


微基准测试类型。JMH 提供了以下几种类型进行支持:



可以注释在方法级别,也可以注释在类级别,


@BenchmarkMode(Mode.All)public class LinkedListIterationBenchMark {...}@Benchmark@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})public void m() {...}


@Warmup


这个单词的意思就是预热,iterations = 3就是指预热轮数。


@Benchmark@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})@Warmup(iterations = 3)public void m() {...}


@Measurement


正式度量计算的轮数。


@Benchmark@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})@Measurement(iterations = 3)public void m() {...}


@Threads


每个进程中的测试线程。

@Threads(Threads.MAX)public class LinkedListIterationBenchMark {...}


@Fork


进行 fork 的次数。如果 fork 数是3的话,则 JMH 会 fork 出3个进程来进行测试。

@Benchmark@BenchmarkMode({Mode.Throughput, Mode.SingleShotTime})@Fork(value = 3)public void m() {...}


@OutputTimeUnit


基准测试结果的时间类型。一般选择秒、毫秒、微秒。

@OutputTimeUnit(TimeUnit.SECONDS)public class LinkedListIterationBenchMark {...}


@Benchmark


方法级注解,表示该方法是需要进行 benchmark 的对象,用法和 JUnit 的 @Test 类似。


@Param


属性级注解,@Param 可以用来指定某项参数的多种情况。特别适合用来测试一个函数在不同的参数输入的情况下的性能。


@Setup


方法级注解,这个注解的作用就是我们需要在测试之前进行一些准备工作,比如对一些数据的初始化之类的。微信搜索公众号:互联网架构师,回复“2T”,获取更多精品学习资料


@TearDown


方法级注解,这个注解的作用就是我们需要在测试之后进行一些结束工作,比如关闭线程池,数据库连接等的,主要用于资源的回收等。


@State


当使用@Setup参数的时候,必须在类上加这个参数,不然会提示无法运行。

就比如我上面的例子中,就必须设置state。

State 用于声明某个类是一个“状态”,然后接受一个 Scope 参数用来表示该状态的共享范围。因为很多 benchmark 会需要一些表示状态的类,JMH 允许你把这些类以依赖注入的方式注入到 benchmark 函数里。Scope 主要分为三种。



启动方法


在启动方法中,可以直接指定上述说到的一些参数,并且能将测试结果输出到指定文件中
 /**     * 仅限于IDE中运行     * 命令行模式 则是 build 然后 java -jar 启动     *     * 1. 这是benchmark 启动的入口     * 2. 这里同时还完成了JMH测试的一些配置工作     * 3. 默认场景下,JMH会去找寻标注了@Benchmark的方法,可以通过include和exclude两个方法来完成包含以及排除的语义     */    public static void main(String[] args) throws RunnerException {        Options opt = new OptionsBuilder()                // 包含语义                // 可以用方法名,也可以用XXX.class.getSimpleName()                .include("Helloworld")                // 排除语义                .exclude("Pref")                // 预热10轮                .warmupIterations(10)                // 代表正式计量测试做10轮,                // 而每次都是先执行完预热再执行正式计量,                // 内容都是调用标注了@Benchmark的代码。                .measurementIterations(10)                //  forks(3)指的是做3轮测试,                // 因为一次测试无法有效的代表结果,                // 所以通过3轮测试较为全面的测试,                // 而每一轮都是先预热,再正式计量。                .forks(3)            .output("E:/Benchmark.log")                .build();

new Runner(opt).run(); }


结语


基于JMH可以对很多工具和框架进行测试,比如日志框架性能对比、BeanCopy性能对比 等,更多的example可以参考官方给出的JMH samples

相关阅读:2T架构师学习资料干货分享



全栈架构社区交流群

 「全栈架构社区」建立了读者架构师交流群,大家可以添加小编微信进行加群。欢迎有想法、乐于分享的朋友们一起交流学习。

扫描添加好友邀你进架构师群,加我时注明姓名+公司+职位】

看完本文有收获?请转发分享给更多人


往期资源:


Flutter 移动应用开发实战 视频(开发你自己的抖音APP)
Java面试进阶训练营 第2季(分布式篇)
Java高级 - 分布式系统开发技术视频
浏览 32
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报