1.5倍是连续内存分配下对复制开销与空间浪费的最优折中。它平衡扩容频次(4次达100)与闲置率(

1.5 倍不是凑出来的数字,而是连续内存分配下对“复制开销”和“空间浪费”的硬性折中。
为什么必须是连续内存视角下的 1.5,而不是 2 或 1.2
ArrayList 的底层数组要求一块连续地址空间。扩容时 JVM 必须分配一块新的连续内存,并把旧数组全部拷贝过去——这个过程无法增量进行,只能整块搬迁。System.arraycopy() 再快,也是 O(n) 时间 + 实际内存带宽消耗。
- 用
2.0倍:容量跳变太猛(10 → 20 → 40 → 80),容易在 45 个元素时浪费 35 个空槽;长期运行后,大量小碎片数组残留,GC 回收压力上升 - 用
1.2倍:10 → 12 → 14 → 16… 从 10 到 100 需要约 24 次扩容,意味着 24 次完整数组拷贝,对百万级日志写入线程就是持续抖动 -
1.5倍(即oldCapacity + (oldCapacity >> 1)):10 → 15 → 22 → 33 → 49 → 73 → 109,从 10 到 100 只需 4 次扩容,且每次闲置空间比例始终压在 30% 以内
扩容触发点不在“满”,而在“add 前一刻”
很多人误以为“加第 11 个才扩容”,其实 add(E) 第一步就调用 ensureCapacityInternal(size + 1) —— 它检查的是“这次插入后会不会越界”,不是“当前是否已满”。
- 无参构造的
ArrayList,第一次add()就触发扩容到10(不是 1.5×0,而是硬编码最小值) - 循环中反复
add()却不预设容量,会在第 11、16、23、34… 次触发扩容,每次都是全量拷贝 -
addAll()传入 100 个元素时,minCapacity直接算出 100,跳过中间所有 1.5 倍步骤,一步到位
1.5 倍在 JVM 内存管理中的隐性协同
Java 堆对大块连续内存的分配与回收有偏好。1.5 倍增长产生的新旧数组大小差值温和(比如 100→150,差 50),旧数组被弃用后更容易被 G1 或 ZGC 识别为“可合并的连续死区”;而 2 倍扩容(100→200)会让旧数组变成一块孤立的中等尺寸碎片,尤其在老年代容易加剧内存整理负担。
- 位运算
oldCapacity >> 1是为了规避浮点计算开销,本质仍是 ×1.5 向下取整 - 当
oldCapacity接近Integer.MAX_VALUE时,1.5 倍可能溢出,此时会退回到Integer.MAX_VALUE或抛OutOfMemoryError - 复制成本取决于元素类型:引用类型只拷贝 8 字节指针,但若开启 ZGC,大量指针搬运仍会触发额外写屏障
真正关键的不是记住“1.5”,而是理解:连续内存不允许试错。每一次扩容都在重画内存地图,而 1.5 是 Java 工程师用二十年 GC 日志和真实业务负载反复校准出来的安全边界。










