深入理解 ArrayList 扩容机制与最大数组长度之谜

ArrayList 的扩容机制

    private Object[] grow(int minCapacity) {
        int oldCapacity = elementData.length;
        if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, /* minimum growth */
                    oldCapacity >> 1           /* preferred growth */);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        } else {
            return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
        }
    }

当向 ArrayList 中添加元素时,它会首先检查当前数组的容量是否足够。具体来说,在执行 add 方法时会判断当前的元素个数(size)是否等于数组容量(elementData.length)。若两者相等,则表示数组已满,需要触发扩容操作。

扩容操作的核心逻辑位于 grow(int minCapacity) 方法中,其参数 minCapacity代表满足当前添加操作所需的最小容量,即 size + 1

方法的执行流程分为两种情况:

  1. 正常扩容(数组已使用过)
    如果原数组容量大于0,或者当前数组不是默认的空数组(意味着已经分配过内存),则会计算新容量。计算规则是:

    • 最小增长量minCapacity - oldCapacity(即至少需要增加多少才能满足需求)
    • 首选增长量oldCapacity >> 1(即原容量的一半,相当于扩容至原容量的1.5倍)
    • ArraysSupport.newLength 方法会从这两个增长量中选取一个较大值,来确定最终的新容量。
    • 确定容量后,通过 Arrays.copyOf 将原数组元素拷贝到新扩容的数组中。
  2. 首次扩容(默认空数组的第一次添加)
    如果数组是默认的空数组(即通过 new ArrayList<>() 创建且从未添加过元素),则直接创建一个新数组。其容量取 DEFAULT_CAPACITY(默认值为10)和 minCapacity 中的较大值。

简而言之,ArrayList 的扩容策略是:优先尝试按1.5倍增长,如果1.5倍不够满足最小需求,则按最小需求增长;如果是首次添加,则直接初始化为默认容量10。

最大数组长度

在阅读源码时,你会发现这么几行代码

       /**
     * A soft maximum array length imposed by array growth computations.
     * Some JVMs (such as HotSpot) have an implementation limit that will cause
     *
     *     OutOfMemoryError("Requested array size exceeds VM limit")
     *
     * to be thrown if a request is made to allocate an array of some length near
     * Integer.MAX_VALUE, even if there is sufficient heap available. The actual
     * limit might depend on some JVM implementation-specific characteristics such
     * as the object header size. The soft maximum value is chosen conservatively so
     * as to be smaller than any implementation limit that is likely to be encountered.
     */
    public static final int SOFT_MAX_ARRAY_LENGTH = Integer.MAX_VALUE - 8;

    public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
        // preconditions not checked because of inlining
        // assert oldLength >= 0
        // assert minGrowth > 0

        int prefLength = oldLength + Math.max(minGrowth, prefGrowth); // might overflow
        if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
            return prefLength;
        } else {
            // put code cold in a separate method
            return hugeLength(oldLength, minGrowth);
        }
    }

为什么这个扩容的最大数据为2147483647 - 8呢?而不是减 16?为什么不是减 4?为什么不是直接用 Integer.MAX_VALUE?

jvm本身的内存对齐

JVM 在分配内存时,要求对象的起始地址必须是 8 字节的整数倍。这叫做 8 字节对齐

假如你要分配为N的对象数组

总内存占用 = 对象头 + N * 每个引用的字节数

实际占用 = 对齐到8的倍数(对象头 + N* 每个引用的字节数)

如果我设置容量为2147483639,即限制的最大数,此时内存占用为(假如是四字节引用)

内存占用: 16 + 2147483639 * 4 = 8 589 934 572

8 589 934 572 mod 8 = 4,所以需要补充四个字节

实际占用为8 589 934 576

那如果是2147483640,岂不是也可以呢?

16 + 2147483640 * 4 mod 8 = 0

虽然2147483640在数学上可行,但是为了保守,JVM做了安全保留

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CHCY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值