Java原生数组无法动态扩容,根本原因是其长度在JVM内存结构中创建时即固定不可变;所谓扩容实为创建新数组、复制有效元素、更新引用、旧数组待GC的内存重建过程。

Java 中的原生数组(如 int[]、String[]、Object[])无法直接动态扩容,根本原因在于:数组是固定长度的对象,其长度在创建时就写死在内存结构中,JVM 不提供修改长度的机制。
当你声明 int[] arr = new int[5];,JVM 就在堆上分配一块连续内存,精确容纳 5 个 int,并把长度 5 存入该数组对象的元数据(类似一个隐藏字段)。这个长度不可更改——不是语法限制,而是 JVM 规范和内存模型的硬性约束。
所以所谓“扩容”,本质上是一次内存重建操作,而非“拉长”原数组。
扩容时的真实物理过程
每次触发扩容(比如 ArrayList.add() 发现 size == elementData.length),实际发生的是:
立即学习“Java免费学习笔记(深入)”;
- 创建一个全新数组,例如从长度 10 扩到 15(1.5 倍)或按需更大;
- 调用
System.arraycopy()把旧数组的全部有效元素(共size个)逐字节拷贝到新数组起始位置; - 将引用(如
elementData)指向新数组; - 原数组失去引用,等待 GC 回收。
这个过程带来三类物理性能损耗:
-
内存分配开销:每次
new Object[newCapacity]都要向堆申请连续内存块,小容量可能快,但大数组易触发 GC 或内存碎片; -
数据复制开销:
System.arraycopy()是本地方法,虽经 JVM 高度优化,但仍需O(n)时间遍历并搬运size个对象引用(或基本类型值); -
GC 压力叠加:旧数组变成“短期存活对象”,若扩容频繁(如循环中逐个
add),会大量产生待回收对象,加剧 Young GC 频率。
举个典型例子:
向空 ArrayList 连续添加 1000 个元素,会经历约 10 次扩容(10 → 15 → 22 → 33 → 49 → 73 → 109 → 163 → 244 → 366 → 549),累计复制元素超 2500 次——这些都不是“免费”的。
为什么不用 for 循环复制?而必须用 System.arraycopy()
手动写 for (int i = 0; i < old.length; i++) new[i] = old[i]; 看似等价,但:
- JVM 对
System.arraycopy()做了深度内联与汇编级优化(如使用rep movsq指令批量移动); - 对于对象数组,它还能跳过某些类型检查和 write barrier(尤其在 ZGC/Shenandoah 下);
- 手写循环无法享受这些优化,实测慢 2–5 倍,且容易引入边界错误。
那“延迟初始化”有什么影响?
ArrayList() 无参构造时,elementData 并不立即分配 10 个空间,而是指向一个共享的 EMPTY_ELEMENTDATA(长度为 0 的静态数组)。
只有第一次 add() 时才真正分配 new Object[10]。
这减少了“只建不填”场景下的内存浪费,但一旦开始添加,那条 if (size == elementData.length) 判断线就立刻生效——扩容逻辑从不因“初始没分配”而绕过。
这意味着:是否扩容,只取决于当前已存元素数(size)和当前物理容量(array.length),和你当初怎么构造它无关。



















