System.arraycopy本身不分配堆内存,只搬运已有内存块;其“零堆分配”可通过-Xlog:gc+allocation=debug无分配日志、-XX:+PrintAssembly显示rep movsq等纯内存指令、-verbose:gc无异常触发来实证。

System.arraycopy 本身不分配堆内存——它只搬运已有内存块,不 new 对象、不创建数组、不触发 GC 分配。所谓“零堆内存分配”,指在调用过程中 JVM 不在堆上申请新对象空间,包括不生成中间数组、不包装、不隐式装箱。
要用 JVM 参数实证这一点,关键不是靠单个参数“开启证明”,而是组合监控手段,排除堆分配行为。以下是可落地的验证路径:
✅ 一、用 -XX:+PrintGCDetails -Xlog:gc* 观察是否触发分配相关 GC
arraycopy 若真引起堆分配(比如因类型检查失败而抛异常并构造异常对象),或目标数组未预分配而误以为它会自动创建,就可能间接引发 GC 日志中的 Allocation Failure。
验证操作:
java -XX:+PrintGCDetails \
-Xlog:gc*,gc+allocation=debug \
-Xmx4m -Xms4m \
YourCopyTest
-
gc+allocation=debug(JDK 10+)会打印每次堆内存分配的调用栈和大小; - 若
arraycopy调用前后无任何allocation日志行(尤其没有new array或Object[]等堆分配记录),说明它自身未触发堆分配; - 注意:
new int[1000]创建源/目标数组时会有分配日志,但arraycopy(...)这一行调用本身不会出现新 allocation 记录。
✅ 正确现象示例:
[0.123s][debug][gc,allocation] Allocation of 4000 bytes succeeded using TLAB← 来自new int[1000][0.125s][debug][gc,allocation]← 关键证据
✅ 二、用 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly(需 hsdis)看 JIT 是否内联为纯内存指令
HotSpot 对 arraycopy 做了 intrinsic 优化,小数组可能被 JIT 编译为 rep movsq(x86-64)等无堆操作的 CPU 指令。
验证操作:
java -XX:+UnlockDiagnosticVMOptions \
-XX:+PrintAssembly \
-XX:CompileCommand=print,java.lang.System.arraycopy \
YourCopyTest
- 输出汇编中若出现
rep movsq、movdqu、vmovdqu等纯寄存器/内存搬运指令,且无call到 Java 分配函数(如Universe::heap()->mem_allocate),即证明该次拷贝全程未触堆; - 大数组可能跳转到
memcpy@plt,仍是 libc 的无堆内存复制。
✅ 典型片段(简化):
0x00007f...: rep movsq # 一次搬 8 字节 × N 次,无堆交互
✅ 三、用 -XX:+TraceClassLoadingPreorder -verbose:gc 排除反射/代理等隐式对象创建
某些错误用法(如把 int[] 往 Object[] 拷)会在运行时报 ArrayStoreException,而异常对象本身需堆分配。这不是 arraycopy 的责任,但会影响“零分配”观感。
验证操作:
java -verbose:gc \
-XX:+TraceClassLoadingPreorder \
YourCopyTest
- 若执行期间无新增类加载(尤其没加载
java.lang.ArrayStoreException相关栈帧)、且 GC 日志安静,则说明arraycopy成功完成,未因类型错误导致异常对象分配。
❌ 补充提醒:什么情况下它 看似 有堆分配?
| 场景 | 原因 | 是否 arraycopy 自身导致 |
|---|---|---|
Arrays.copyOf(arr, len) 被调用 |
内部先 new 数组,再 arraycopy
|
❌ 分配来自 new,非 arraycopy
|
src 或 dest 是刚 new 出来的数组 |
分配发生在 arraycopy 调用前 |
❌ 属于用户代码逻辑 |
| 拷贝对象数组时触发 write barrier | 可能更新卡表(Card Table),但不分配新对象 | ⚠️ 属于 GC 协作,非堆内存申请 |
System.arraycopy 的“零堆分配”本质是它的契约:你提供两块合法内存区域,它只做位移,不越界、不构造、不解释。JVM 参数不能“打开零分配开关”,但可通过上述日志与汇编,清晰看到它确实绕过了堆管理路径——这才是最硬的证明。










