• ArrayList扩容源码解析


    环境信息
    jdk: 1.8.0_341
    macOs 64位

    介绍

    ArrayListList接口的一个实现,内部基于数组数据结构存储数据;随着元素被添加到ArrayList,它的容量会自动增长,也就是说ArrayList的容量是可以动态调整的,当空间不够用时,默认增加容量的50%。

    在性能方面,size, isEmpty, get, set, iterator, and listIterator等操作总是run in constant time(即O(1));add操作则是amortized constant time,也就是说,添加1个元素需要O(1)的时间,添加n个元素就需要O(n)的时间。对于剩下的其它操作,粗略来讲都是linear time(O(n))。

    ArrayList不能保证线程安全,如果要在多线程环境下使用ArrayList,则应该使用VectorCollections.synchronizedList(new ArrayList(...));

    关键属性

    以下是和扩容相关的一些属性:

    public class ArrayList<E> extends AbstractList<E>
            implements List<E>, RandomAccess, Cloneable, java.io.Serializable
    {
    
        /**
         * 真正用来存储元素的数组,ArrayList的capacity就是这个数组的长度。
         * 注:ArrayList没有capacity这个属性,代码中以elementData.length代表capacity。
         */
        transient Object[] elementData;
        /**
         * 仅使用无参构造器后,添加第一个元素时,容量会默认扩展为10。
         */
        private static final int DEFAULT_CAPACITY = 10;
        /**
         * ArrayList内已有元素的个数。
         * capacity是ArrayList能存放的最大元素个数;size是已有的实际个数。
         */
        private int size;
    
        /**
         * 使用无参构造器时,elementData会被初始化为DEFAULTCAPACITY_EMPTY_ELEMENTDATA,之后添加元素时代码会
         * 判断element == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,决定是否扩展至DEFAULT_CAPACITY。
         */
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    
        /**
         * 使用有参构造器时,如果传入的初始容量为0或传入空Collection,element会被赋值为EMPTY_ELEMENTDATA。
         */
        private static final Object[] EMPTY_ELEMENTDATA = {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    源码分析

    add方法

    public boolean add(E e) {
        // 确保容量,如果容量不够先扩容
        ensureCapacityInternal(size + 1);
        // size初始值为0,当调用add时将该元素放到index为0的位置。之后size++为1,代表当前ArryList内元素个数为1,且下次添加元素的index为1。
        elementData[size++] = e;
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    往下看ensureCapacityInternal的内容:

    /**
     * minCapacity:代表当前数组至少也要有这个容量才可以,否则就扩容。
     *  如果调用方时`add(E e)`方法,则`minCapacity`传入值为当前`size + 1`;
     *  如果调用方为`addAll(Collection c)`,`minCapacity`则为`size + c.size()`。
     */
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        
        // `minCapacity`大于`ArrayList`的容量时需要扩容。
        if (minCapacity - elementData.length > 0)
          	// 扩容
            grow(minCapacity);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    grow扩容方法

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
          	// 扩容
            grow(minCapacity);
    }
    
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
      
        // 容量增加50%,(oldCapacity >> 1)等于(oldCapacity / 2)
        int newCapacity = oldCapacity + (oldCapacity >> 1);
      
        // 如果容量增加50%仍不够用,则使用minCapacity作为新容量。
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
      
        // 如果newCapacity大于MAX_ARRAY_SIZE,则尝试为它分配MAX_ARRAY_SIZE或Integer.MAX_VALUE的大小。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
      
        // 创建一个大小为newCapacity的新数组,并将元素统统拷贝过去。
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    扩容代码里有几个细节需要我们注意:

    1. 为什么不直接把MAX_ARRAY_SIZE设置为Integer.MAX_VALUE,非要等到newCapacity大于MAX_ARRAY_SIZE时才扩容至Integer.MAX_VALUE
    2. 源码中注释overflow-conscious code想表达什么意思?以及为什么用a - b > 0,而不用a > b
    MAX_ARRAY_SIZE

    先来看第一个问题:通常情况下,ArrayList不会返回大于MAX_ARRAY_SIZE的容量,除非给定的minCapacity大于MAX_ARRAY_SIZE

    原因是因为现在很多JVM的实现(例如常用的hotspot)对申请大小为Integer.MAX_VALUE的数组有限制,会抛出OutOfMemoryError("Requested array size exceeds VM limit") 异常。这个限制值的大小取决于JVM实现的特征,例如对象头大小。所有的JVM的实现最大就限制到了Integer.MAX_VALUE - 8,所以JDK保守的选择了最大值8来应对遇到的所有限制。

    Object[] arr = new Object[Integer.MAX_VALUE];
    // 异常:java.lang.OutOfMemoryError: Requested array size exceeds VM limit。-- jvm会直接限制住。
            
    Object[] arr = new Object[Integer.MAX_VALUE - 8];
    // 异常:java.lang.OutOfMemoryError: Java heap space。 -- jvm不会限制,尝试分配内存结果堆溢出。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    overflow-conscious code 与 a - b > 0
    // 此示例代码引自参考目录编号2的文章。
    int a = Integer.MAX_VALUE;
    int b = Integer.MIN_VALUE;
    if (a < b) {
        System.out.println("a < b");
    }
    if (a - b < 0) {
        System.out.println("a - b < 0");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在上面这个例子中,控制台会打印a - b < 0。显然,a < b是错误的。而a - b发生了溢出,结果为-1,所以会打印出a - b < 0

    在jdk7就存在由这个问题导致的bug,我们不妨看下jdk7的扩容源码。

    // jdk 7 源码
    public boolean add(E e) {
        ensureCapacity(size + 1);
        elementData[size++] = e;
        return true;
    }
    
    public void ensureCapacity(int minCapacity) {
        modCount++;
        int oldCapacity = elementData.length;
        if (minCapacity > oldCapacity) {
            int newCapacity = (oldCapacity * 3)/2 + 1;
            if (newCapacity < minCapacity)
                newCapacity = minCapacity;
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    这个BUG很容易重现:假设当前的capacity至少为 Integer.MAX_VALUE的三分之二,则(oldCapacity * 3)/2 + 1的结果会溢出为负数,此时newCapacity < minCapacitytruenewCapacity被设置为minCapacity。那这样会造成什么问题呢?

    在这种情况调用add方法,capacity只会+1,而不是增加50%,而且每次add操作都会触发扩容导致性能下降(Arrays.copyOf)。


    理解了上面后,我们再看JDK8的grow方法 ,为了方便阅读,我把代码再贴一次。

    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    ensureExplicitCapacity方法里,需要注意的是入参minCapacity可能已经溢出为负数。无论minCapacity是真的大于elementData.length,或是溢出后与elementData.length相减再溢出为正数,都会进入到grow方法。

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
      
        int newCapacity = oldCapacity + (oldCapacity >> 1);    -----代码①
    
        if (newCapacity - minCapacity < 0)    ----- 代码②
            newCapacity = minCapacity;		----- 代码③
              
        if (newCapacity - MAX_ARRAY_SIZE > 0)   ----- 代码④
            newCapacity = hugeCapacity(minCapacity);
      
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    如果代码①处newCapacity溢出为负数,而且minCapacity是正数的话,代码②处会再次溢出为正数(因为minCapacity - elementData.length > 0)。到代码④处,同样溢出为正数,最后会在hugeCapacity里返回Integer.MAX_VALUEMAX_ARRAY_SIZE

    当然,minCapacity也有可能是负数,在这个前提下,无论代码②处结果如何,代码④都会溢出为正数,然后在hugeCapacity抛出异常。

    以上两种情况都是在newCapacity溢出的前提下。

    但是!其实这里是有BUG的(JDK后续版本已修复),我们把代码精简如下:

    private static int calculate(final int minCapacity, final int length) {
    	if (minCapacity - length > 0) {
    		int oldCapacity = length;
    		int newCapacity = oldCapacity + (oldCapacity >> 1);
    		if (newCapacity - minCapacity < 0) {
    			newCapacity = minCapacity;
    		}
    		if (newCapacity - (Integer.MAX_VALUE - 8) > 0) {
    			if (minCapacity < 0) {
    				throw new OutOfMemoryError();
    			}
    			newCapacity = minCapacity > (Integer.MAX_VALUE - 8) ?
    				Integer.MAX_VALUE :
    				(Integer.MAX_VALUE - 8);
    		}
    		return newCapacity;
    	}
    	return length;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    当入参是calculate((Integer.MAX_VALUE - 2) + (Integer.MAX_VALUE - 2), (Integer.MAX_VALUE - 2))时,结果会返回-6。

    可以运行如下代码验证,代码抛出NegativeArraySizeException异常:

    // -Xmx17g
    public static void main(String[] args) {
      	// Integer.MAX_VALUE - 4: hugeCapacity 抛出 OutOfMemoryError.
    		// Integer.MAX_VALUE - 2 or - 3:,申请负数长度的数组,报错:NegativeArraySizeException
    		// Integer.MAX_VALUE - 1: OutOfMemoryError: Requested array size exceeds VM limit
        int size = Integer.MAX_VALUE - 2;
        ArrayList<Object> huge = new ArrayList<>(size);
        for (int i = 0; i < size; i++)
            huge.add(null);
        try {
            huge.addAll(huge);
        } catch (OutOfMemoryError e) {
          	e.printStackTrace();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    上面这个测试需要内存 > 32g,可以改掉ArrayList的源码,这样内存只需要16g就可以了,改动如下:

    public Object[] toArray() {
        return Arrays.copyOf(elementData, size);
    }
    
    改为
      
    public Object[] toArray() {
        return elementData;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    到此扩容方法就讲完了,内容如果有问题,也希望大佬们不吝赐教,共同进步。

    参考目录

    1: Java SE API Documentation

    2: Difference between if (a - b < 0) and if (a < b)

  • 相关阅读:
    线上MySQL的自增id用完了怎么办?
    国货之光——jdchain1.6.5测试网络部署
    一文让你掌握Vue的状态管理机Vuex
    实验1:使用Matlab工具箱进行相机标定实验
    eclipse中创建并运行Filter和Listener
    【Spring】Spring的创建和使用手术刀剖析
    java项目之交通事故档案管理系统(源码+文档)
    sysbench
    pm2 使用方法,常用命令,及设置vue等前端项目开启启动,node,eggjs 等node项目开机启动
    im即时通讯开发之双进程守护保活实践
  • 原文地址:https://blog.csdn.net/MariaOzawa/article/details/127707631