System.arraycopy越界不抛异常的两种情况:length为0时静默跳过;堆外内存越界写入可读写页内仅篡改邻近字段,导致线程无限循环卡死,引发全站假死。

一次 System.arraycopy 越界,没抛出异常,却让整个服务响应卡死、线程池耗尽、监控失灵——这不是玄学,而是真实发生在某金融中台的“静默假死”事故。根本原因不是越界本身,而是越界触发了底层 JVM 的不可恢复状态,再叠加缺乏分层容错,最终演变成全站级雪崩。
越界不报错?那是你没踩对点
System.arraycopy 确实会在多数情况下直接抛 ArrayIndexOutOfBoundsException,但有两类关键场景它不会立即崩溃:
- 当
length == 0时,无论srcPos或dstPos多离谱(比如负数或远超数组长度),JVM 都会静默跳过,不校验也不报错 - 当目标数组是通过
Unsafe.allocateMemory或 JNI 直接分配的堆外内存,且越界写入落在可读写页内,可能只改掉邻近变量值,不触发信号
事故中正是后者:一段加密模块用 DirectByteBuffer 做临时缓冲区,arraycopy 的 dstPos 计算错误导致覆盖了紧邻的 capacity 字段。后续所有对该 buffer 的 put() 操作都因容量被篡改为极大负数而无限循环在边界检查里,线程彻底卡住。
单点越界如何滚成全站假死
问题不在越界动作本身,而在系统缺少防御纵深:
- 没有线程级超时:处理单个请求的线程卡死后,既不中断也不释放,持续占用 Tomcat 工作线程
- 无熔断降级:下游依赖(如风控规则引擎)因上游线程阻塞无法及时返回,连锁拖垮调用链路
- 健康检查失效:/actuator/health 仍返回 UP,因为主线程和心跳线程未受影响,运维看到的是“一切正常”的假象
结果就是:QPS 从 1200 直线跌到 30,错误率显示为 0%,而用户端大量 504 和白屏——典型的“无错误假死”。
企业级容错不能只靠 try-catch
把 arraycopy 包一层 try-catch 不仅无效,反而有害:
- 它捕不到静默越界,也救不了已损坏的内存结构
- 高频路径加异常捕获会显著增加 JIT 编译压力,降低吞吐
- 掩盖了真正的校验缺失——越界本该在进入拷贝前就杜绝
真正有效的做法是分层设防:
- 输入校验层:所有外部传入的 pos/length 参数,在进入任何 array 操作前统一做
checkIndex(src, srcPos, length)校验 - 内存契约层:关键缓冲区(如加解密、序列化)强制使用封装类(如
SafeArrayView),构造时即冻结长度与有效区间 - 运行时防护层:在 JVM 启动参数中加入
-XX:+UseMembar -XX:+AlwaysPreTouch,减少因内存页未映射导致的隐性延迟
不复杂但容易忽略。

















