
System.arraycopy() 实现高性能片段搬运,核心不在“怎么写”,而在“它根本没执行你写的那行 Java 代码”——JIT 编译器在运行时直接把它替换成 CPU 指令。理解这点,才能真正用好它。
片段搬运的正确写法与关键约束
搬运不是语法糖,是精确的内存操作,五个参数必须严丝合缝:
src 和 dest 都不能为空,否则抛 NullPointerException srcPos ≥ 0 且 srcPos + length ≤ src.length;destPos ≥ 0 且 destPos + length ≤ dest.length,越界立即抛 ArrayIndexOutOfBoundsException 类型兼容性不可妥协:int[] 不能拷到 long[];String[] 可拷到 Object[],但反过来不行;泛型擦除不影响判断,List<String>[] 拷到 Object[] 是合法的 length = 0 是合法且无开销的,适合做空校验或占位逻辑
重叠搬运(如数组内平移)的安全机制
System.arraycopy 支持源与目标为同一数组且区间重叠,语义等价于 memmove 而非 memcpy:
当 destPos > srcPos(向右位移),JVM 自动从高下标往低下标复制,避免覆盖未读数据 当 destPos < srcPos(向左位移),自动正序拷贝,确保安全 例如把 arr[5]~arr[9](共 5 个元素)右移 3 位到 arr[8]~arr[12],只需:System.arraycopy(arr, 5, arr, 8, 5); 它不检查“你想干什么”,只按你传的坐标执行——方向错误、长度算错、越界,全由你负责
JIT 内联与硬件级优化的真实原理
它的快,不是因为“native”,而是因为 HotSpot C2 编译器在热方法编译阶段,查 intrinsic 表后彻底跳过 Java 层调用栈,生成高度定制的机器码:
边界检查只做一次,在入口处完成所有合法性校验(空数组、索引、长度、类型),后续全是裸指针访存 x86-64 上可能展开为 rep movsq(对齐的 8 字节块搬)、或带 movaps/movdqu 的 SIMD 循环;AArch64 上常用 ldp/stp 成对加载存储,SVE 向量指令在支持时自动启用 若源/目标地址自然对齐(如 16/32/64 字节),JIT 会自动选用更宽向量单元(AVX-512 下单次搬 64 字节) 插入硬件预取(prefetch)指令,提升缓存命中率;手写循环需靠 Unsafe.prefetchRead,且效果难控 @IntrinsicCandidate 注解只是“报名表”,是否生效取决于 JVM 版本、C2 是否启用、CPU 特性检测结果(如 -XX:+UseAVX 未开启则不用 AVX 指令)
性能对比与使用边界
实测中差异显著,但优势有前提:
对 int[] 拷贝 100 万个元素(JDK 17 / Linux x86_64): • System.arraycopy:约 0.03–0.05 ms • 手写 for 循环(局部变量缓存):约 0.12–0.18 ms • 增强 for 循环:约 0.25–0.40 ms(迭代器开销+频繁边界检查) 优势随数组增大而放大;byte[] 差距更明显,可逼近 memcpy 极限 小数组(≤ 4 元素)或需条件过滤/转换时,arraycopy 不适用;反复调用它拷 1 个元素反而因 JNI 开销得不偿失 对象数组搬运涉及 GC 写屏障(尤其跨代引用),性能优势弱于基本类型;此时浅拷贝语义也需开发者自行管理生命周期

评论(0)