system.arraycopy 处理大对象数组时性能优势显著,因其仅复制引用地址、一次性完成类型与边界校验、避免循环开销,且在≥256元素时比for循环快2.5–5倍;但需注意浅拷贝语义、gc卡表更新、缓存行竞争等隐藏开销。

只拷引用,不拷对象——浅拷贝的本质开销
对 `Object[]` 这类对象数组,`arraycopy` 复制的是每个元素的引用地址(8 字节指针),不是对象实例内容。这意味着:
- 耗时与数组长度成正比,与单个对象大小无关——拷 10 万个 `ArrayList` 引用,和拷 10 万个 `String` 引用,时间几乎一样
- 不触发 GC:没创建新对象,不增加堆压力
- 但后续修改风险仍在:若某个 `ArrayList` 被目标数组和源数组同时持有,往里 add 元素,两边都可见——这不是开销,是语义,必须由业务逻辑兜底
类型检查和边界校验是一次性成本,不是累加开销
调用前 JVM 会做五项检查:src/dest 非 null、srcPos/destPos ≥ 0、length ≥ 0、不越界(srcPos + length ≤ src.length 等)、类型兼容(如 `String[]` → `Object[]` 合法,反之不行)。这些检查只做一次,不随数组变大而变慢。一旦通过,就直接进 native 层搬内存块。
所以——大数组反而更划算:校验成本被摊薄,JIT 更可能启用 SIMD 指令(如一次搬 16 字节),缓存预取效率更高。
真正影响性能的隐藏开销
- GC 卡表更新(Card Table):若源或目标数组位于老年代,跨代拷贝可能触发写屏障记录。G1/ZGC 下影响较小,CMS 下较明显;可通过 `-XX:+UseG1GC` 和预分配降低干扰
- CPU 缓存行竞争:多线程并发写同一 dest 数组不同区域,看似隔离,实则可能落在同一缓存行(64 字节),引发 false sharing,拖慢整体吞吐
- 内存不对齐:若目标数组由 `Unsafe.allocateMemory` 或非标准方式分配且未对齐到 8/16 字节边界,JVM 可能降级为逐字节拷贝,失去向量化优势
- TLB miss:超大数组(如 512MB+)跨越多个内存页,频繁换页表项,尤其在物理内存紧张时明显
对比 for 循环:为什么大对象数组仍推荐 arraycopy
手动 for 循环每轮都要:
- 两次数组边界检查(读 src[i] 和写 dest[j] 各一次)
- 一次 null 检查(对象数组默认开启)
- 一次引用赋值的写屏障(取决于 GC 算法)
- JIT 难以稳定向量化——循环体含分支、副作用不确定
`arraycopy` 把这些全剥离,换成一次 native 调用 + 一块连续内存搬运。实测在 ≥ 256 个元素时,通常快 2.5–4 倍;到 10⁵ 级别,差距稳定在 3–5 倍。











