这类OOM的本质是RPC框架反序列化时因长度字段校验缺失,错误申请超大数组(如new byte[Integer.MAX_VALUE])导致堆内存瞬间占满;需聚焦“谁申请巨数组”和“为什么申请”,通过JVM日志、线程栈及协议层校验定位并修复。
这类 oom 的本质是:rpc 框架在反序列化请求时,因对变长参数(object... 或 byte[]...)的长度字段校验缺失或误读,错误地申请了一个超大容量的空数组(如 new byte[integer.max_value]),瞬间占满堆内存。排查需聚焦“谁申请了巨数组”和“为什么申请”,而非泛泛看内存泄漏。
定位巨型数组的创建现场
关键不是等 OOM 后分析 dump,而是提前捕获数组分配行为:
- 启动 JVM 时添加
-XX:+UnlockDiagnosticVMOptions -XX:+LogVMOutput -XX:LogFile=jvm.log,配合-XX:+TraceClassLoading和-XX:+PrintGCDetails,观察 OOM 前是否出现异常大的数组分配日志 - 用
jstack -l <pid></pid>抓取线程栈,重点看处于RUNNABLE状态且调用栈含read、deserialize、decode的线程,确认其正卡在反序列化入口 - 若框架基于 Thrift/Protobuf/gRPC 封装,检查其
readXXX()方法中是否有类似int len = input.readI32(); byte[] buf = new byte[len];的逻辑 —— 这里len若未做上限校验,就是高危点
验证变长参数解析逻辑缺陷
直接审查 RPC 框架中处理 varargs 的反序列化代码段:
- 查找所有接收
...Object、...byte[]、...T的服务方法签名,逆向追踪其对应的反序列化器(如ThriftCodec.decodeVarargs()) - 检查长度读取后是否做了合理约束,例如:
错误写法:int n = is.readShort(); byte[] arr = new byte[n];
正确写法:int n = is.readShort(); if (n 1024 * 1024) throw new ProtocolException("Invalid varargs length"); byte[] arr = new byte[n]; - 注意网络字节序转换(如
readInt()返回的是 big-endian,若误当 little-endian 解析,可能将0x000000FF解为16777215,触发 16MB 数组分配)
复现与隔离验证
不依赖线上故障,主动构造边界 case 验证:
- 用抓包工具(如 Wireshark)截获一次正常 RPC 请求,修改其二进制 payload 中表示 varargs 元素个数的字段(如将 2 字节长度域设为
0xFFFF),重放请求到测试环境 - 在框架反序列化层加断点,观察
len变量值、new byte[len]执行前后的堆内存变化(用jstat -gc <pid></pid>监控 Eden 区突增) - 若使用自研序列化协议,检查协议文档中 varargs 长度字段定义:是否明确最大值?是否允许 0 或负数?框架实现是否与文档一致?
修复与防护措施
修复必须从协议层和实现层双管齐下:
- 在反序列化入口强制设置长度白名单,例如全局限制 varargs 元素数 ≤ 1000,单个 byte[] ≤ 8MB,超出即抛
ProtocolException并记录告警 - 将数组分配改为惰性加载:先读长度 → 校验 → 仅当真正需要访问某索引时才分配对应子数组,避免一次性全量分配
- 在 RPC 网关或服务治理层增加 payload 大小熔断,对单次请求总长度 > 16MB 的直接拒绝,防止恶意构造绕过框架校验

















