Java中使用HashMap时指定初始化容量性能解析

 更新时间:2023年02月05日 10:50:55   作者:xindoo  
这篇文章主要为大家介绍了Java中使用HashMap时指定初始化容量性能解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

正文

一些Java编程老手在做CodeReview时,都会告诉其他人,使用HashMap时建议指定容量大小,原因是指定容量后,代码性能会更好一些。后来随着阿里Java开发手册在业内广为传播,这一点早已深入人心,我自己也早已习惯在使用HashMap时指定容量大小。

但我今天突发奇想,想知道指定容量和不指定容量时性能究竟有多少的差异,测试部分测试数据的结果让我大跌眼睛,有些情况下指定容量的性能还比不指定容量时差!! ,但其他部分还是很符合我之前的认知的。

openjdk17和jmh单线程测试

先说下我的测试平台和测试方法,我使用了openjdk17和jmh单线程测试,测试代码如下:

    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @Measurement(iterations = 2, time = 5)
    @Threads(1)
    @Fork(0)
    @Warmup(iterations = 1, time = 5)
    public void withoutCap() {
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < CAP; i++) {
            map.put(random.nextInt(), 1);
        }
    }
    @Benchmark
    @BenchmarkMode(Mode.Throughput)
    @Measurement(iterations = 2, time = 5)
    @Threads(1)
    @Fork(0)
    @Warmup(iterations = 1, time = 5)
    public void withCap() {
        Map<Integer, Integer> map = new HashMap<>(CAP);
        for (int i = 0; i < CAP; i++) {
            map.put(random.nextInt(), 1);
        }
    }

这里为了避免Java中小数据缓存,我特意使用了随机数作为KEY,而VALUE一视同仁都使用了1。两个方法就是新建一个HashMap并不断往map里put数据,唯一差异就是一个指定了CAP参数。 在我设置了不同参数后,得到了以下数据(越高越好):

数据量不指定容量(ops/s)指定容量(ops/s)
25109543324000032
42516175611813275
8107671765900641
1629783742987958
3212316371545394
64567643764260
256129350185540
10242747535799
10252719568466
409666819937
327688071177
65536377567

可以看出,容量16是个分水岭,当容量为16时,二者几乎没啥差异,这也很容易理解,当不指定容量时默认初始容量就是16。

  • 但容量大于16时,指定容量时的性能会高于不指定时的性能,随着数量的增加,前者会比后者性能高出50%。
  • 但当数据量小于16时,不指定容量大小反而性能更高,最多甚至相差2倍,这就和我们之前的认知不一样了。

上面数据中还有个很奇怪的点,那就是当数据量为1025时,性能居然还高于1024,而且差异巨大。就好比别人比你多干了1份活,但用的时间比你少一半。我跑了多次都是这个结果,这不是测试误差,这个结果和计算机底层存储实现有关,具体原理可以参考问题 为什么转置512x512的矩阵比转置513x513的矩阵慢?

备注:以上数据经过多次运行测试,数据虽有波动,但数据波动基本都在3%以内。

那为什么在大数据量的情况下,指定容量的代码性能会更好呢?这就得说到HashMap的实现原理,更详细内容可以参考我之前写的HashMap源码浅析。这里为了方便大家直观地理解性能差异产生的原因,我们用牧场养羊类比下。

假设你要开始养羊,你得现有场地吧,假设你先找了块小场地,但随着你的羊群发展壮大,场地不够用了,你就得搬到一个更大的新场地,如果发展速度特别快,你就得频繁搬家,搬家就逐渐变成了负担。但如果你一开始就知道你最多能养多少的羊,直接找个足够大的场地,不就能省去一直搬家的成本了吗!

这里你把羊类比成数据,场地类比为内存,在HashMap中,如果开始不指定容量大小,JVM默认会给你一个非常小的(16)的容量空间,如果之后数据量变多,就需要重新申请更大的空间,并把数据迁移到新空间上,于是额外增加了时间消耗。这便是性能差异产生的原因。

但当容量小于16时,指定容量的方式反而性能更差。这个我之前从未看过其他资料有说过,我简单谈下自己的分析和理解。 当调用new HashMap()和new HashMap(CAP)时,分别执行了不同的构造函数,而二者的构造函数的逻辑是有差异的,当指定容量时,执行了容量参数检查的代码:

    public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }
    static final int tableSizeFor(int cap) {
        int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

不指定容量时,构造方法内只有一行this.loadFactor = DEFAULT_LOAD_FACTOR;,在put的数据量一致时,后续所有的代码执行流程都是一致的,所以指定容量时,上面容量参数检查的代码带来了额外的性能负担,所以导致数据量较小时指定容量时反而性能更差一些。

总结

最后回到文章标题上来,Java中使用HashMap时指定初始化容量性能一定会更好嘛?答案是不一定,指定容量也有可能性能会更差。当然,绝大多数情况下还是建议指定容量的,类似的还有ArrayList,也建议指定容量。 别人给出的结论不一定的完全正确的,只有知道产生结论的原因,才能更有效的利用这个结论。

以上就是Java中使用HashMap时指定初始化容量性能解析的详细内容,更多关于Java HashMap容量性能的资料请关注脚本之家其它相关文章!

相关文章

  • 详解Java解析XML的四种方法

    详解Java解析XML的四种方法

    本篇文章主要介绍了java解析XML的几种方式,XML现在已经成为一种通用的数据交换格式,给数据集成与交互提供了方便,有需要的可以了解一下。
    2016-11-11
  • java stream实现分组BigDecimal求和以及自定义分组求和

    java stream实现分组BigDecimal求和以及自定义分组求和

    这篇文章主要给大家介绍了关于java stream实现分组BigDecimal求和以及自定义分组求和的相关资料,Stream是Java8的一大亮点,是对容器对象功能的增强,它专注于对容器对象进行各种非常便利、高效的聚合操作或者大批量数据操作,需要的朋友可以参考下
    2023-12-12
  • 学习java一定要知道的垃圾收集器

    学习java一定要知道的垃圾收集器

    这篇文章主要介绍了学习java一定要知道的垃圾收集器,垃圾收集器的发展路线,简单来说是随着内存越来越大而发生变化,更多相关介绍需要的朋友可以参考一下
    2022-07-07
  • Springboot处理异常的常见方式

    Springboot处理异常的常见方式

    SpringBoot框架异常处理有多种处理方式,今天就带大家了解一下常见的springboot异常处理方式,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-06-06
  • MyBatis中的ResultMap的association和collection标签详解

    MyBatis中的ResultMap的association和collection标签详解

    这篇文章主要介绍了MyBatis中的ResultMap的association和collection标签详解,主要包括association标签常用参数及id & result标签参数详解,本文给大家介绍的非常详细,需要的朋友可以参考下
    2022-10-10
  • SpringBoot连接MYSQL数据库并使用JPA进行操作

    SpringBoot连接MYSQL数据库并使用JPA进行操作

    今天给大家介绍一下如何SpringBoot中连接Mysql数据库,并使用JPA进行数据库的相关操作。
    2017-04-04
  • SpringBoot2.x 整合 thumbnailator 图片处理的示例代码

    SpringBoot2.x 整合 thumbnailator 图片处理的示例代码

    这篇文章主要介绍了SpringBoot2.x 之整合 thumbnailator 图片处理,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-10-10
  • 面试初级Java开发问到Arrays

    面试初级Java开发问到Arrays

    这篇文章主要介绍了Java Arrays工具类用法,结合实例形式分析了java Arrays工具类针对数组元素修改、复制、排序等操作使用技巧与相关注意事项,需要的朋友可以参考下
    2021-07-07
  • Java Handler同步屏障浅析讲解

    Java Handler同步屏障浅析讲解

    同步屏障机制是什么?Handler发送的消息分为普通消息、屏障消息、异步消息,一旦Looper在处理消息时遇到屏障消息,那么就不再处理普通的消息,而仅仅处理异步的消息。不再使用屏障后,需要撤销屏障,不然就再也执行不到普通消息了
    2022-08-08
  • SpringBoot2整合JTA组件实现多数据源事务管理

    SpringBoot2整合JTA组件实现多数据源事务管理

    这篇文章主要介绍了SpringBoot2整合JTA组件实现多数据源事务管理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03

最新评论