架构师Java 并发基准测试神器的-JMH,程序员必看!


在Java编程这个行业里面性能测试这个话题非常庞大,我们可以从网络聊到操作系统,再从操作系统聊到内核,再从内核聊到你怀疑人生有木有。

先拍几个砖出来吧,我在写代码的时候经常有这种怀疑:写法A快还是写法B快,某个位置是用ArrayList还是LinkedList,HashMap还是TreeMap,HashMap的初始化size要不要指定,指定之后究竟比默认的DEFAULT_SIZE性能好多少。。。

如果你还是通过for循环或者手撸method来测试你的内容的话,那么JMH就是你必须要明白的内容了,因为已经有人把基准测试的轮子造好了,接下来我们就一起看看这个轮子怎么用:

JMH只适合细粒度的方法测试,并不适用于系统之间的链路测试!

JMH只适合细粒度的方法测试,并不适用于系统之间的链路测试!

JMH只适合细粒度的方法测试,并不适用于系统之间的链路测试!

JMH入门:

JMH是一个工具包,如果我们要通过JMH进行基准测试的话,直接在我们的pom文件中引入JMH的依赖即可:

    <dependency>

        <groupId>org.openjdk.jmh</groupId>

        <artifactId>jmh-core</artifactId>

        <version>1.19</version>

    </dependency>

    <dependency>

        <groupId>org.openjdk.jmh</groupId>

        <artifactId>jmh-generator-annprocess</artifactId>

        <version>1.19</version>

    </dependency>

通过一个HelloWorld程序来看一下JMH如果工作:

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

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

public class JMHSample_01_HelloWorld {

static class Demo {

    int id;

    String name;

    public Demo(int id, String name) {

        this.id = id;

        this.name = name;

    }

}

static List<Demo> demoList;

static {

    demoList = new ArrayList();

    for (int i = 0; i < 10000; i ++) {

        demoList.add(new Demo(i, "test"));

    }

}

@Benchmark

@BenchmarkMode(Mode.AverageTime)

@OutputTimeUnit(TimeUnit.MICROSECONDS)

public void testHashMapWithoutSize() {

    Map map = new HashMap();

    for (Demo demo : demoList) {

        map.put(demo.id, demo.name);

    }

}

@Benchmark

@BenchmarkMode(Mode.AverageTime)

@OutputTimeUnit(TimeUnit.MICROSECONDS)

public void testHashMap() {

    Map map = new HashMap((int)(demoList.size() / 0.75f) + 1);

    for (Demo demo : demoList) {

        map.put(demo.id, demo.name);

    }

}

public static void main(String[] args) throws RunnerException {

    Options opt = new OptionsBuilder()

            .include(JMHSample_01_HelloWorld.class.getSimpleName())

            .forks(1)

            .build();

    new Runner(opt).run();

}

}

======================================执行结果======================================

Benchmark Mode Cnt Score Error Units

JMHSample_01_HelloWorld.testHashMap avgt 5 147.865 ± 81.128 us/op

JMHSample_01_HelloWorld.testHashMapWithoutSize avgt 5 224.897 ± 102.342 us/op

======================================执行结果======================================

上面的代码用中文翻译一下:分别定义两个基准测试的方法testHashMapWithoutSize和 testHashMap,这两个基准测试方法执行流程是:每个方法执行前都进行5次预热执行,每隔1秒进行一次预热操作,预热执行结束之后进行5次实际测量执行,每隔1秒进行一次实际执行,我们此次基准测试测量的是平均响应时长,单位是us。

预热?为什么要预热?因为 JVM 的 JIT 机制的存在,如果某个函数被调用多次之后,JVM 会尝试将其编译成为机器码从而提高执行速度。为了让 benchmark 的结果更加接近真实情况就需要进行预热。

从上面的执行结果我们看出,针对一个Map的初始化参数的给定其实有很大影响,当我们给定了初始化参数执行执行的速度是没给定参数的2/3,这个优化速度还是比较明显的,所以以后大家在初始化Map的时候能给定参数最好都给定了,代码是处处优化的,积少成多。

通过上面的内容我们已经基本可以看出来JMH的写法雏形了,后面的介绍主要是一些注解的使用:

@Benchmark

@Benchmark标签是用来标记测试方法的,只有被这个注解标记的话,该方法才会参与基准测试,但是有一个基本的原则就是被@Benchmark标记的方法必须是public的。

@Warmup

@Warmup用来配置预热的内容,可用于类或者方法上,越靠近执行方法的地方越准确。一般配置warmup的参数有这些:

?iterations:预热的次数。

?time:每次预热的时间。

?timeUnit:时间单位,默认是s。

?batchSize:批处理大小,每次操作调用几次方法。(后面用到)

@Measurement

用来控制实际执行的内容,配置的选项本warmup一样。

@BenchmarkMode

@BenchmarkMode主要是表示测量的纬度,有以下这些纬度可供选择:

?Mode.Throughput 吞吐量纬度

?Mode.AverageTime 平均时间

?Mode.SampleTime 抽样检测

?Mode.SingleShotTime 检测一次调用

?Mode.All 运用所有的检测模式 在方法级别指定@BenchmarkMode的时候可以一定指定多个纬度,例如:@BenchmarkMode({Mode.Throughput, Mode.AverageTime, Mode.SampleTime, Mode.SingleShotTime}),代表同时在多个纬度对目标方法进行测量。

?

@OutputTimeUnit

@OutputTimeUnit代表测量的单位,比如秒级别,毫秒级别,微妙级别等等。一般都使用微妙和毫秒级别的稍微多一点。该注解可以用在方法级别和类级别,当用在类级别的时候会被更加精确的方法级别的注解覆盖,原则就是离目标更近的注解更容易生效。

@State

在很多时候我们需要维护一些状态内容,比如在多线程的时候我们会维护一个共享的状态,这个状态值可能会在每隔线程中都一样,也有可能是每个线程都有自己的状态,JMH为我们提供了状态的支持。该注解只能用来标注在类上,因为类作为一个属性的载体。@State的状态值主要有以下几种:

?Scope.Benchmark 该状态的意思是会在所有的Benchmark的工作线程中共享变量内容。

?Scope.Group 同一个Group的线程可以享有同样的变量

?Scope.Thread 每隔线程都享有一份变量的副本,线程之间对于变量的修改不会相互影响。下面看两个常见的@State的写法:

1.直接在内部类中使用@State作为“PropertyHolder”

public class JMHSample_03_States {

@State(Scope.Benchmark)

public static class BenchmarkState {

    volatile double x = Math.PI;

}

@State(Scope.Thread)

public static class ThreadState {

    volatile double x = Math.PI;

}

@Benchmark

public void measureUnshared(ThreadState state) {

    state.x++;

}

@Benchmark

public void measureShared(BenchmarkState state) {

    state.x++;

}

public static void main(String[] args) throws RunnerException {

    Options opt = new OptionsBuilder()

            .include(JMHSample_03_States.class.getSimpleName())

            .threads(4)

            .forks(1)

            .build();

    new Runner(opt).run();

}

}

2.在Main类中直接使用@State作为注解,是Main类直接成为“PropertyHolder”

@State(Scope.Thread)

public class JMHSample_04_DefaultState {

double x = Math.PI;

@Benchmark

public void measure() {

    x++;

}

public static void main(String[] args) throws RunnerException {

    Options opt = new OptionsBuilder()

            .include(JMHSample_04_DefaultState.class.getSimpleName())

            .forks(1)

            .build();

    new Runner(opt).run();

}

}

我们试想以下@State的含义,它主要是方便框架来控制变量的过程逻辑,通过@State标示的类都被用作属性的容器,然后框架可以通过自己的控制来配置不同级别的隔离情况。被@Benchmark标注的方法可以有参数,但是参数必须是被@State注解的,就是为了要控制参数的隔离。

但是有些情况下我们需要对参数进行一些初始化或者释放的操作,就像Spring提供的一些init和destory方法一样,JHM也提供有这样的钩子:

[email protected] 必须标示在@State注解的类内部,表示初始化操作

[email protected] 必须表示在@State注解的类内部,表示销毁操作

?

初始化和销毁的动作都只会执行一次。

@State(Scope.Thread)

public class JMHSample_05_StateFixtures {

double x;

@Setup

public void prepare() {

    x = Math.PI;

}

@TearDown

public void check() {

    assert x > Math.PI : "Nothing changed?";

}

@Benchmark

public void measureRight() {

    x++;

}

public static void main(String[] args) throws RunnerException {

    Options opt = new OptionsBuilder()

            .include(JMHSample_05_StateFixtures.class.getSimpleName())

            .forks(1)

            .jvmArgs("-ea")

            .build();

    new Runner(opt).run();

}

}

虽然我们可以执行初始化和销毁的动作,但是总是感觉还缺点啥?对,就是初始化的粒度。因为基准测试往往会执行多次,那么能不能保证每次执行方法的时候都初始化一次变量呢?@Setup和@TearDown提供了以下三种纬度的控制:

?Level.Trial 只会在个基础测试的前后执行。包括Warmup和Measurement阶段,一共只会执行一次。

?Level.Iteration 每次执行记住测试方法的时候都会执行,如果Warmup和Measurement都配置了2次执行的话,那么@Setup和@TearDown配置的方法的执行次数就4次。

?Level.Invocation 每个方法执行的前后执行(一般不推荐这么用)

?

@Param

在很多情况下,我们需要测试不同的参数的不同结果,但是测试的了逻辑又都是一样的,因此如果我们编写镀铬benchmark的话会造成逻辑的冗余,幸好JMH提供了@Param参数来帮助我们处理这个事情,被@Param注解标示的参数组会一次被benchmark消费到。

@State(Scope.Benchmark)

public class ParamTest {

@Param({"1", "2", "3"})

int testNum;

@Benchmark

public String test() {

    return String.valueOf(testNum);

}

public static void main(String[] args) throws RunnerException {

    Options opt = new OptionsBuilder()

            .include(ParamTest.class.getSimpleName())

            .forks(1)

            .build();

    new Runner(opt).run();

}

}

@Threads

测试线程的数量,可以配置在方法或者类上,代表执行测试的线程数量。

通常看到这里我们会比较迷惑Iteration和Invocation区别,我们在配置Warmup的时候默认的时间是的1s,即1s的执行作为一个Iteration,假设每次方法的执行是100ms的话,那么1个Iteration就代表10个Invocation。

JMH进阶

通过以上的内容我们已经基本可以掌握JMH的使用了,下面就主要介绍一下JMH提供的一些高级特性了。

不要编写无用代码

因为现代的编译器非常聪明,如果我们在代码使用了没有用处的变量的话,就容易被编译器优化掉,这就会导致实际的测量结果可能不准确,因为我们要在测量的方法中避免使用void方法,然后记得在测量的结束位置返回结果。这么做的目的很明确,就是为了与编译器斗智斗勇,让编译器不要改变这段代码执行的初衷。

Blackhole介绍

Blackhole会消费传进来的值,不提供任何信息来确定这些值是否在之后被实际使用。Blackhole处理的事情主要有以下几种:

?死代码消除:入参应该在每次都被用到,因此编译器就不会把这些参数优化为常量或者在计算的过程中对他们进行其他优化。

?处理内存壁:我们需要尽可能减少写的量,因为它会干扰缓存,污染写缓冲区等。这很可能导致过早地撞到内存壁

?

我们在上面说到需要消除无用代码,那么其中一种方式就是通过Blackhole,我们可以用Blackhole来消费这些返回的结果。

1:返回测试结果,防止编译器优化

@Benchmark

public double measureRight_1() {

return Math.log(x1) + Math.log(x2);

}

2.通过Blackhole消费中间结果,防止编译器优化

@Benchmark

public void measureRight_2(Blackhole bh) {

bh.consume(Math.log(x1));

bh.consume(Math.log(x2));

}

循环处理

我们虽然可以在Benchmark中定义循环逻辑,但是这么做其实是不合适的,因为编译器可能会将我们的循环进行展开或者做一些其他方面的循环优化,所以JHM建议我们不要在Beanchmark中使用循环,如果我们需要处理循环逻辑了,可以结合@BenchmarkMode(Mode.SingleShotTime)和@Measurement(batchSize = N)来达到同样的效果.

@State(Scope.Thread)

public class JMHSample_26_BatchSize {

List<String> list = new LinkedList<>();

// 每个iteration中做5000次Invocation

@Benchmark

@Warmup(iterations = 5, batchSize = 5000)

@Measurement(iterations = 5, batchSize = 5000)

@BenchmarkMode(Mode.SingleShotTime)

public List<String> measureRight() {

    list.add(list.size() / 2, "something");

    return list;

}

@Setup(Level.Iteration)

public void setup(){

    list.clear();

}

public static void main(String[] args) throws RunnerException {

    Options opt = new OptionsBuilder()

            .include(JMHSample_26_BatchSize.class.getSimpleName())

            .forks(1)

            .build();

    new Runner(opt).run();

}

}

方法内联

方法内联:如果JVM监测到一些小方法被频繁的执行,它会把方法的调用替换成方法体本身。比如说下面这个:

private int add4(int x1, int x2, int x3, int x4) {

    return add2(x1, x2) + add2(x3, x4);

}

private int add2(int x1, int x2) {

    return x1 + x2;

}

运行一段时间后JVM会把add2方法去掉,并把你的代码翻译成:

private int add4(int x1, int x2, int x3, int x4) {

    return x1 + x2 + x3 + x4;

}

JMH提供了CompilerControl注解来控制方法内联,但是实际上我感觉比较有用的就是两个了:

?CompilerControl.Mode.DONT_INLINE:强制限制不能使用内联

?CompilerControl.Mode.INLINE:强制使用内联 看一下官方提供的例子把:

?

@State(Scope.Thread)

@BenchmarkMode(Mode.AverageTime)

@OutputTimeUnit(TimeUnit.NANOSECONDS)

public class JMHSample_16_CompilerControl {

public void target_blank() {

}

@CompilerControl(CompilerControl.Mode.DONT_INLINE)

public void target_dontInline() {

}

@CompilerControl(CompilerControl.Mode.INLINE)

public void target_inline() {

}

@Benchmark

public void baseline() {

}

@Benchmark

public void dontinline() {

    target_dontInline();

}

@Benchmark

public void inline() {

    target_inline();

}

public static void main(String[] args) throws RunnerException {

    Options opt = new OptionsBuilder()

            .include(JMHSample_16_CompilerControl.class.getSimpleName())

            .warmupIterations(0)

            .measurementIterations(3)

            .forks(1)

            .build();

    new Runner(opt).run();

}

}

======================================执行结果==============================

Benchmark Mode Cnt Score Error Units

JMHSample_16_CompilerControl.baseline avgt 3 0.896 ± 3.426 ns/op

JMHSample_16_CompilerControl.dontinline avgt 3 0.344 ± 0.126 ns/op

JMHSample_16_CompilerControl.inline avgt 3 0.391 ± 2.622 ns/op

======================================执行结果==============================

重磅!码农突围-技术交流群已成立

大家可添加码农突围助手,可申请加入码农突围大群和细分方向群,细分方向已涵盖:Java、Python、机器学习、大数据、人工智能等群。

专注于Java架构师技术分享,撩我免费送Java全套架构师晋级资料

(Java架构师交流企*----:445--820-*-908 )

原文地址:https://blog.51cto.com/14667748/2468714

时间: 2024-11-03 10:12:55

架构师Java 并发基准测试神器的-JMH,程序员必看!的相关文章

java架构师之路:JAVA程序员必看的15本书的电子版下载地

转自:http://www.shangxueba.com/faq/view376.html 作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水平的Java程序员们. 一.Java编程入门类 对于没有Java编程经验的程序员要入门,随便读什么入门书籍都一样,这个阶段需要你快速的掌握Java基础语法和基本用法,宗旨就是“囫囵吞枣不求甚解”,先对Java

Java架构师分享自己的技术体系,程序员如何从码农到专家

一.源码分析 源码分析是一种临界知识,掌握了这种临界知识,能不变应万变,源码分析对于很多人来说很枯燥,生涩难懂. 源码阅读,我觉得最核心有三点:技术基础+强烈的求知欲+耐心. 我认为是阅读源码的最核心驱动力.我见到绝大多数程序员,对学习的态度,基本上就是这几个层次(很偏激哦): 下图是我总结出目前最应该学习的源码知识点: 二.分布式架构 分布式系统是一个复杂且宽泛的研究领域,学习一两门在线课程,看一两本书可能都是不能完全覆盖其所有内容的. 总的来说,分布式系统要做的任务就是把多台机器有机的组合.

BAT架构师2019年最新总结,从程序员到CTO,从专业走向卓越

2019年最新总结,从程序员到CTO,从专业走向卓越,大牛分享文档pdf与PPT整理 整理大牛分享文档如下,持续更新一线开发架构,技术文档 下载地址:https://github.com/0voice/from_coder_to_expert 网易蜂巢公有容器云架构之路 新浪微博redis优化历程 微博Cache架构设计实践 Go在大数据开发中的经验总结 基于Go构建滴滴核心业务平台的实践 Go in TiDB 负载均衡利器 HAProxy功能剖析及部署案例 高可用技术的实践分享 高性能存储及文

JAVA程序员必看的15本书-JAVA自学书籍推荐

作为Java程序员来说,最痛苦的事情莫过于可以选择的范围太广,可以读的书太多,往往容易无所适从.我想就我自己读过的技术书籍中挑选出来一些,按照学习的先后顺序,推荐给大家,特别是那些想不断提高自己技术水平的Java程序员们.此外,大家可以加入457036818交流群,互相分享一下关于JAVA方面的知识.一.Java编程入门类 对于没有Java编程经验的程序员要入门,随便读什么入门书籍都一样,这个阶段需要你快速的掌握Java基础语法和基本用法,宗旨就是"囫囵吞枣不求甚解",先对Java熟悉

JAVA程序员必看11本书籍

http://developer.51cto.com/art/201512/503095.htm 学习的最好途径就是看书",这是我自己学习并且小有了一定的积累之后的第一体会.个人认为看书有两点好处: 1.能出版出来的书一定是经过反复的思考.雕琢和审核的,因此从专业性的角度来说,一本好书的价值远超其他资料 2.对着书上的代码自己敲的时候方便 "看完书之后再次提升自我的最好途径是看一些相关的好博文",我个人认为这是学习的第二步,因为一本书往往有好几百页,好的博文是自己看书学习之后

武汉java培训:大牛Java程序员必看书籍

学java程序员,大部头的书籍是技术升级的必备工具,对于有基础的java程序员,市面上适合初学者的大量书籍男入法眼,武汉java培训专家为大家分享了一份高阶的java书籍,拿走不谢: 下面我分享的书单绝对值得拥有.我尽力避免列出为特定软件或框架或认证的Java书,因为我觉得那不是纯Java书. 1.<Java in a Nutshell>(Java技术手册) 与其说是必读书籍,还不说是参考文献. 2.<The elements of Java style>(Java编程风格) 目标

39套精品Java从入门到架构师|高并发|高性能|高可用|分布式|集群|电商缓存|性能调优|设计项目实战|视频教程

精品Java高级课,架构课,java8新特性,P2P金融项目,程序设计,功能设计,数据库设计,第三方支付,web安全,高并发,高性能,高可用,分布式,集群,电商,缓存,性能调优,设计模式,项目实战,大型分布式电商项目实战视频教程   视频课程包含: 39套Java精品高级课架构课包含:java8新特性,P2P金融项目,程序设计,功能设计,数据库设计,架构设计,web安全,高并发,高性能,高可用,高可扩展,分布式,集群,电商,缓存,性能调优,设计模式,项目实战,工作流,程序调优,负载均衡,Solr

【转】成为Java顶尖程序员 ,看这11本书就够了

成为Java顶尖程序员 ,看这11本书就够了 转自:http://developer.51cto.com/art/201512/503095.htm 以下是我推荐给Java开发者们的一些值得一看的好书.但是这些书里面并没有Java基础.Java教程之类的书,不是我不推荐,而是离我自己学习 Java基础技术也过去好几年了,我学习的时候看的什么也忘了,所以我不能不负责任地推荐一些我自己都没有看过的书给大家. 作者:来源:攻城狮之家|2015-12-31 09:55 收藏 分享 “学习的最好途径就是看

Java从入门到架构师|高并发|高性能|高可用|分布式|性能调优|设计模式|大型电商项目

没有设计的思想,你就不能成为一名架构师.架构师是一个能撸的了一手好代码,画的了一个漂亮的UML/原型,写的了一篇技术文档,更加能解决好项目关键技术的综合人才.架构师=前端工程师+后端程序员+系统分析师+关键技术解决+各种技术搭配+设计模式+部署调优+其他,可见架构师是多面手,在项目当中起到连接管理与项目成员的重要角色.因此,在通往大神级的架构师的道路上,你需要懂需求.设计.代码.部署.架构.服务器.运维.调优等等. 简单系统架构图 一个能担负起企业级应用的架构师,脑海里常出现的词会是这些:负载均