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容量性能的资料请关注脚本之家其它相关文章!

相关文章

  • SpringBoot整合Quartz方法详解

    SpringBoot整合Quartz方法详解

    这篇文章详解介绍了SpringBoot整合Quartz的方法,Quartz是一个比较成熟了的定时任务框架,本文实例代码给大家详细讲解,需要的朋友可以参考下
    2023-04-04
  • 使用Nacos实现动态路由的步骤和代码示例

    使用Nacos实现动态路由的步骤和代码示例

    这篇文章主要介绍了使用 Nacos 实现 Spring Cloud Gateway 的动态路由,本文给大家介绍了具体的实现步骤和代码案例,感兴趣的小伙伴跟着小编一起来看看吧
    2024-09-09
  • springboot多文件上传代码实例及解析

    springboot多文件上传代码实例及解析

    这篇文章主要介绍了springboot多文件上传代码实例及解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-12-12
  • 搭建Spring MVC和Vue3的应用程序的实现

    搭建Spring MVC和Vue3的应用程序的实现

    本文主要介绍了搭建Spring MVC和Vue3的应用程序的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-11-11
  • 如何使用try-with-resource机制关闭连接

    如何使用try-with-resource机制关闭连接

    这篇文章主要介绍了使用try-with-resource机制关闭连接的操作,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-07-07
  • springboot+mybatis配置clickhouse实现插入查询功能

    springboot+mybatis配置clickhouse实现插入查询功能

    这篇文章主要介绍了springboot+mybatis配置clickhouse实现插入查询功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-08-08
  • 利用Spring Boot和JPA创建GraphQL API

    利用Spring Boot和JPA创建GraphQL API

    这篇文章主要介绍了利用Spring Boot和JPA创建GraphQL API,GraphQL既是API查询语言,也是使用当前数据执行这些查询的运行时,下文更多相关内容介绍需要的小伙伴可以参考一下
    2022-04-04
  • springboot使用redis对单个对象进行自动缓存更新删除的实现

    springboot使用redis对单个对象进行自动缓存更新删除的实现

    本文主要介绍了springboot使用redis对单个对象进行自动缓存更新删除的实现,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • Spring MVC框架配置方法详解

    Spring MVC框架配置方法详解

    这篇文章主要为大家详细介绍了Spring MVC框架的配置方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-04-04
  • Java基础之java处理ip的工具类

    Java基础之java处理ip的工具类

    这篇文章主要介绍了Java基础应用,使用java处理ip的工具类的相关资料,需要的朋友可以参考下
    2014-10-10

最新评论