system.arraycopy不是零拷贝,也不支持numa优化;真正高性能需规避冗余拷贝、内存按节点亲和分配、线程与内存严格绑定。

别再指望 arraycopy 实现零拷贝
arraycopy 是 JVM 内部调用 CPU 指令(如 rep movsb)完成的用户态内存拷贝,全程走 CPU,不绕过内存总线,也不感知 NUMA 节点。它和 sendfile、mmap、DMA 等真正的零拷贝技术有本质区别。把它称作“零拷贝”,属于概念混淆。
实际瓶颈往往不在拷贝本身,而在拷贝背后的对象创建、反射调用、链式 DTO 转换等高开销操作。一次 arraycopy 可能只花 100ns,但前面 new 对象 + GC 压力可能拖到微秒级。
让 arraycopy 的代价降到最低
即使无法避免拷贝,也能把单次代价压到稳定低水平:
- 启用 JVM NUMA 支持:加参数 -XX:+UseNUMA -XX:NUMAGranularity=2M,让 G1/ZGC 按节点划分堆区域
- 确保源数组和目标数组都分配在同一个 NUMA 节点上——用 numa_alloc_onnode()(C/C++/JNI 层)或 ByteBuffer.allocateDirect() 配合 Unsafe.copyMemory 显式控制物理位置
- 实测表明:同节点内 arraycopy 延迟可稳在 80–120ns;跨节点则飙升至 300ns 以上,且带宽受限于 QPI/Infinity Fabric 总线
比优化 arraycopy 更有效的三件事
与其调优一个本就不该高频出现的操作,不如从源头消除它:
- 跳过字节数组中转:序列化环节改用 FlatBuffers 或 Cap’n Proto,字段直接内存映射访问,无需构造对象、也无需 byte[] 中间载体
- 网络 payload 零搬运:用 JNI 接入 DPDK 或 AF_XDP,让网卡 mbuf 的 payload 指针直接指向预分配的本地 NUMA 大页,彻底绕开 JVM 堆和 arraycopy
-
用结构化视图替代复制:比如用 std::span
或 MemorySegment(Java 21+)绑定已有内存,仅变更逻辑视图,不搬数据
三者对齐才是 NUMA 收益的开关
NUMA 不是“开了就快”的功能,而是需要数据、线程、缓存行三者物理同域才能生效:
- 大数组按节点分片分配,不共享全局堆数组
- Netty EventLoop 或 ForkJoinPool 工作线程用 taskset 或 pthread_setaffinity_np() 绑定到对应 CPU 核心组
- 避免“线程在 node 0、数据在 node 1”这种典型反模式——哪怕没 arraycopy,一次普通字段读取也可能触发远程内存访问











