首先我们运行一段代码:

此时运行,程序正常,接下来我们将注释放开:

此时运行发现,OOM了:

为什么new出来HashMap的时候并没有报OOM,而是在第一次进行put操作的时候才报的OOM?我们来看下map的源码。

设置数组长度、扩容阈值

创建Map时可以指定元素个数,也可以不指定。先来看下不指定元素个数的构造方法:

public HashMap() {    this.loadFactor = DEFAULT_LOAD_FACTOR; // 设置加载因子为0.75}static final float DEFAULT_LOAD_FACTOR = 0.75f;

当进行put操作时,会进行resize扩容操作:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,               boolean evict) {    Node[] tab; Node p; int n, i;    if ((tab = table) == null || (n = tab.length) == 0) //初始table为空        n = (tab = resize()).length;    //触发resize操作    //省略...    }    //省略...}transient Node[] table;

看下resize扩容的核心代码:

final Node[] resize() {    Node[] oldTab = table;    //初始table为空    int oldCap = (oldTab == null) ? 0 : oldTab.length;    //所以oldCap为0    int oldThr = threshold;    //初始threshold为0 所以oldThr为0    int newCap, newThr = 0;    if (oldCap > 0) {        //省略...    }    else if (oldThr > 0)         //省略...    else {                       newCap = DEFAULT_INITIAL_CAPACITY;    //设置新数组长度为16        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);  //设置新map可容纳元素个数为16*0.75    }    if (newThr == 0) {        //省略...    }    threshold = newThr;    //为threshold赋值为newThr 代表扩容后的map的可容纳元素个数上限 当map中元素个数超过threshold会触发扩容操作    @SuppressWarnings({"rawtypes","unchecked"})        Node[] newTab = (Node[])new Node[newCap];    table = newTab;    //设置table为扩容后的新数组    //省略...    return newTab;}static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

如果指定元素个数,例如:

Map<String,String> map = new HashMap<String,String>(17);
public HashMap(int initialCapacity) {    this(initialCapacity, DEFAULT_LOAD_FACTOR);    //加载因子0.75}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;    //加载因子0.75    this.threshold = tableSizeFor(initialCapacity);    //设置threshold为最接近initialCapacity的2次幂的值}static final float DEFAULT_LOAD_FACTOR = 0.75f;

调用tableSizeFor方法为threshold赋值,看下tableSizeFor的代码:

static final int tableSizeFor(int cap) {    int n = cap - 1;    n |= n >>> 1;    n |= n >>> 2;    n |= n >>> 4;    n |= n >>> 8;    n |= n >>> 16;    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

这个函数是用来对你申请的容量进行处理让他变成最接近你申请的容量的2次幂的大小,这里注意:假如你申请的容量为0,最后处理的结果会变成1,代表着你最小的容量为1。

我们自己测试一下,tableSizeFor(17)=32:

public static void main(String[] args) {    int number = 17;    System.out.println(tableSizeFor(number)); //32}public static int tableSizeFor(int number) {    int n = number - 1;    n |= n >>> 1;    n |= n >>> 2;    n |= n >>> 4;    n |= n >>> 8;    n |= n >>> 16;    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;}

补充:

算术右移(>>)

右移是按最左边(高位)来补的(即如果是1就补1,如果是0就补0,不改变该位的值)。另一种说法,正数右移高位补0,负数右移高位补1。

逻辑右移(>>>)

不管最左边一位是0还是1,都补0。另一种说法,无论是正数还是负数,高位通通补0。

同样的,当put元素时会触发map的扩容操作。

接下来看下指定元素个数时,Map扩容的核心源码。

final Node[] resize() {    Node[] oldTab = table;    int oldCap = (oldTab == null) ? 0 : oldTab.length; //初始table为空 oldCap=0    int oldThr = threshold;     //此时threshold已经有值(2的n次幂)    int newCap, newThr = 0;    if (oldCap > 0) {        //省略...    }    else if (oldThr > 0)         newCap = oldThr;    //扩容后map的数组长度    else {                       省略...    }    if (newThr == 0) {        float ft = (float)newCap * loadFactor;  //设置扩容后map的threshold        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?                  (int)ft : Integer.MAX_VALUE);    }    threshold = newThr;     @SuppressWarnings({"rawtypes","unchecked"})    Node[] newTab = (Node[])new Node[newCap];    table = newTab;    //省略...    return newTab;}

总结一下,如果没有指定元素个数,则扩容后的数组长度默认为16,如果指定元素个数,则扩容后的数组长度为与指定的元素个数最接近的2次幂的值,比如指定元素个数为17,则数组长度为32。threshold的取值都是固定数组长度*加载因子(0.75,0.75并不是一个绝对的,只是在时间和空间上可能map的效率最高)。

再回到一开始的问题,new的时候没有OOM但是在put的时候却OOM了,因为在new的时候只是设置threshold值,而且设置的值还是比最大整数大的2次幂,在扩容的时候,需要分配数组内存,所以OOM了。

数据迁移

上面其实只是分析了resize操作关于设置数组长度、扩容阈值的代码,真正扩容后数据迁移都省略了,接下来看下数据迁移部分:

final Node[] resize() {    //省略...    if (oldTab != null) {        for (int j = 0; j < oldCap; ++j) {    //遍历老数组            Node e;            if ((e = oldTab[j]) != null) {                oldTab[j] = null;                if (e.next == null)                    newTab[e.hash & (newCap - 1)] = e;    //如果老数组的某一索引位置没有链表,则将计算该索引位置的元素在新数组的索引位置                else if (e instanceof TreeNode)                    ((TreeNode)e).split(this, newTab, j, oldCap);                    else {     //如果索引位置有链表                    Node loHead = null, loTail = null;                    Node hiHead = null, hiTail = null;                    Node next;                    do {                        next = e.next;                        if ((e.hash & oldCap) == 0) {    //将索引位置的链表拆分成loHead->loTail和hiHead->hiTail两个链表,顺序保持不变                            if (loTail == null)                                loHead = e;                            else                                loTail.next = e;    //尾插法                            loTail = e;                        }                        else {                            if (hiTail == null)                                hiHead = e;                            else                                hiTail.next = e;                            hiTail = e;                        }                    } while ((e = next) != null);                    if (loTail != null) {                        loTail.next = null;                        newTab[j] = loHead;    //新数组索引位置放入loHead链表                    }                    if (hiTail != null) {                        hiTail.next = null;                        newTab[j + oldCap] = hiHead;    //新数组索引位置+原始数组长度位置放入hiHead链表                    }                }            }        }    }    return newTab;}

举例说明下e.hash&oldCap==0来区分该放到loHead还是hiHead链表,下面是我的理解:

0 1 0 0 0  8    //假设原数组长度8 即oldCap=80 0 0 1 1  3  j=0    //假设原数组0索引位置存在3 6 12 三个数组成的链表0 0 1 1 0  6  j=0 3->6    //经e.hash&oldCap计算 3和6结果均为0 所以组成loHead:3-60 1 1 0 0  12 j=8 [0+8]    //而12与8与操作结果不等于0 所以组成hiHead:12在扩容时 将3->6放入新数组的0索引位置,将12放入新数组的8索引位置

这样的好处是不用计算链表的每一个元素在新数组对应的索引位置了,同时也保持了元素在链表中的顺序。

扩容是元素还是数组_Map扩容源码相关推荐

  1. 寻找第K大元素的八大算法、源码及拓展

    寻找第K大元素的八大算法.源码及拓展 http://www.cnblogs.com/bethunebtj/p/4861378.html 一.问题描述 所谓"第(前)k大数问题"指的 ...

  2. C++实现circular queue循环队列(使用数组)(附完整源码)

    C++实现circular queue循环队列使用数组 C++实现circular queue循环队列(使用数组)算法完整源码(定义,实现,main函数测试) C++实现circular queue循 ...

  3. 萌元素动漫导航网站源码html版

    介绍: 二次元,萌元素动漫导航网站源码html版 网盘下载地址: https://zijiewangpan.com/OOmKxXSIwlu 图片:

  4. 扩容是元素还是数组_02 数组(附ArrayList源码分析)

    定义 用一组连续的内存空间存储一组具有相同类型的数据的线性表数据结构. 优势 支持通过下标快速的随机访问数据,时间复杂度为O(1). 劣势 通常情况下,插入和删除效率低下,每次操作后,需要进行后续元素 ...

  5. hashmap 扩容是元素还是数组_曹工说JDK源码(1)--ConcurrentHashMap,扩容前大家同在一个哈希桶,为啥扩容后,你去新数组的高位,我只能去低位?...

    如何计算,一对key/value应该放在哪个哈希桶 大家都知道,hashmap底层是数组+链表(不讨论红黑树的情况),其中,这个数组,我们一般叫做哈希桶,大家如果去看jdk的源码,会发现里面有一些变量 ...

  6. 扩容是元素还是数组_348,数据结构1,数组

    基础知识 数组是具有相同类型的数据的集合,也就是说数组的所有元素的类型都是相同的,在所有的数据结构中,数组算是最常见也是最简单的一种数据结构,我们最常见的也就是一维数组,当然还有二维,三维--,数组需 ...

  7. hashmap 扩容是元素还是数组_HashMap 中的容量与扩容实现

    总有人心里有火炬,而且彼此能看见. 高手过招,招招致命 JDK1.8 中 HashMap 的底层实现,我相信大家都能说上来个 一二,底层数据结构 数组 + 链表(或红黑树) ,源码如下/** * 数组 ...

  8. hashmap 扩容是元素还是数组_谈谈HashMap扩容

    为什么需要扩容? 因为HashMap为了节省创建出的对象的内存占用,一开始只默认分配: static final int DEFAULT_INITIAL_CAPACITY=1<<4; 也就 ...

  9. hashmap 扩容是元素还是数组_HashMap的扩容机制---resize()

    面试的时候闻到了Hashmap的扩容机制,之前只看到了Hasmap的实现机制,补一下基础知识,讲的非常好 原文链接: Hashmap是一种非常常用的.应用广泛的数据类型,最近研究到相关的内容,就正好复 ...

最新文章

  1. iOS开发之UIDevice通知
  2. HTML和CSS基础知识
  3. 窄带信号和宽带信号的区别和联系
  4. CSP认证201412-2 Z字形扫描[C++题解]:模拟
  5. Java I/O系统学习系列二:输入和输出
  6. CAN'T TAKE MY EYES OF YOU
  7. springboot使用TemplateEngine修改邮箱后发送验证码示例
  8. freemarker的测试结果框架_java必背综合知识点总结(框架篇)
  9. DUMP文件分析4:栈溢出
  10. 算法移植优化(三)android dlib 人脸检测使用
  11. mysql 存储过程(提供查询语句并返回查询执行影响的行数)
  12. java项目_值得学习和练手的Java企业级开源项目,强烈推荐!
  13. 我的第一个博客正式注册
  14. linux 文件系统的简单操作
  15. 利达主机如何注册设备_Kubernetes Pod 如何获取 IP 地址
  16. android-studio安装及android开发环境搭建
  17. 微信小程序:高德地图搜索周边poi接口实践
  18. Java为什么需要数据类型
  19. 算法提高 盾神与积木游戏
  20. OpenSearch 简单学习

热门文章

  1. android学习日记13--数据存储之ContentProvide
  2. jQuery对checkbox的操作(转载)
  3. 《C Traps and Pitfalls》 笔记
  4. 微信网页开发配置步骤
  5. Android 10分钟集成极光推送
  6. docker删除所有镜像和容器
  7. mac安装mysql记录,使用zsh
  8. html检查输入为空,html input输入验证不为空
  9. fopen -- 打开文件或者 URL
  10. ubuntu mysql混合开发_mysql5.7主从同步 ubuntu