欢迎光临
我们一直在努力

OOM 排查复盘:Hutool 序列化 Request 导致 Java Heap Space

OOM 排查复盘:Hutool 序列化 Request 导致 Java Heap Space

1. 问题现象

生产日志中出现堆内存溢出:

java.lang.OutOfMemoryError: Java heap space

主要异常时间点:

2026-06-02 09:46:21
2026-06-02 09:46:40
2026-06-02 09:56:59
2026-06-02 10:01:56
2026-06-02 10:04:32
2026-06-02 10:10:01
2026-06-02 10:12:00
2026-06-02 11:11:09
2026-06-02 11:12:44

日志中多次出现接口:

/xxxxx

其中第一次明确 OOM 栈如下:

java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3689)
at java.util.ArrayList.grow(ArrayList.java:238)
at java.util.ArrayList.add(ArrayList.java:486)
at cn.hutool.json.JSONArray.add(JSONArray.java:388)
at cn.hutool.json.JSONArray.init(JSONArray.java:633)
at cn.hutool.json.JSONUtil.wrap(JSONUtil.java:787)
at cn.hutool.json.JSONObject.set(JSONObject.java:359)
at cn.hutool.core.bean.copier.BeanToMapCopier.copy(BeanToMapCopier.java:49)
at cn.hutool.core.bean.BeanUtil.beanToMap(BeanUtil.java:689)

这说明堆溢出发生在 Hutool JSON 序列化对象时。

2. 涉及代码

问题代码位于操作日志切面:

private void setRequestValue(JoinPoint joinPoint, OperLog operLog) {
Map<String, String[]> paramsMap = httpServletRequest.getParameterMap();
if (paramsMap.isEmpty()) {
Object[] args = joinPoint.getArgs();
String params = JSONUtil.toJsonStr(args);
operLog.setOperParam(params);
} else {
operLog.setOperParam(JSONUtil.toJsonStr(paramsMap));
}
}

登录接口是 @RequestBody 请求:

public Response<LoginVo> login(@Validated @RequestBody LoginDto loginDto,
HttpServletRequest servletRequest) {
return sysUserService.login(loginDto);
}

由于 @RequestBody JSON 参数不会进入:

httpServletRequest.getParameterMap()

所以 paramsMap.isEmpty() 为 true,代码会执行:

Object[] args = joinPoint.getArgs();
JSONUtil.toJsonStr(args);

而 args 中包含:

LoginDto
HttpServletRequest

Hutool 会把 HttpServletRequest / TongWeb 内部 Request 当成普通 Bean 递归展开,最终创建大量 JSONObject、JSONArray、ArrayList、Byte 对象,导致堆被撑爆。

3. HPROF 分析结果

使用 heap dump:

D:\\xxxxx\\doc\\heap202606021028.hprof

对象统计中发现:

java.lang.Byte 87,997,414 个
java.lang.Object[] retained heap 约 862.6 MB
byte[] payload 约 123.3 MB
cn.hutool.json.JSONObject 10,331 个
cn.hutool.json.JSONArray 3,814 个

关键不是 JSONObject 数量特别大,而是 Hutool 在展开 Request 内部字节缓冲、请求结构时,把大量 byte 数据包装成了 java.lang.Byte 对象,并放进 JSONArray / ArrayList。

4. MAT 中的关键证据

在 MAT 的 Leak Suspects 或对象引用链中,找到可疑对象:

cn.hutool.json.JSONObject @ 0xa004b4e0

查看 Incoming References 后发现它被 Hutool copier 持有:

target cn.hutool.core.bean.copier.BeanToMapCopier

再对 BeanToMapCopier 查看:

List Objects -> with outgoing references

看到关键字段:

source com.tongweb.coyote.Request
target cn.hutool.json.JSONObject
targetType class cn.hutool.json.JSONObject
copyOptions cn.hutool.core.bean.copier.CopyOptions

这就是最关键证据。

它说明 Hutool 正在执行:

com.tongweb.coyote.Request -> cn.hutool.json.JSONObject

也就是把 TongWeb 的底层 Request 对象转换成 JSON。

这与 LogAspect 中的:

JSONUtil.toJsonStr(joinPoint.getArgs())

完全吻合。

5. Incoming References 和 Outgoing References 怎么看

Incoming References

含义:

谁引用了当前对象

用于回答:

这个对象为什么还没有被 GC?
是谁持有了它?

例如:

JSONObject <- BeanToMapCopier.target <- Java Local TaskThread

表示该 JSONObject 被 BeanToMapCopier 持有,而 BeanToMapCopier 又被请求线程局部变量持有。

Outgoing References

含义:

当前对象引用了谁

用于回答:

这个对象内部装了什么?
它正在处理哪个 source?

例如对 BeanToMapCopier 查看 outgoing references:

source com.tongweb.coyote.Request
target cn.hutool.json.JSONObject
copyOptions …

这能直接说明 Hutool 正在把 Request 转成 JSONObject。

6. 标准排查步骤

步骤 1:先看日志中的 OOM 栈

搜索:

OutOfMemoryError
Java heap space
JSONUtil
JSONArray
JSONObject
BeanToMapCopier

如果看到:

cn.hutool.json.JSONArray
cn.hutool.json.JSONObject
cn.hutool.core.bean.copier.BeanToMapCopier

说明 OOM 与 Hutool JSON 对象转换高度相关。

步骤 2:打开 MAT 的 Leak Suspects

查看 Problem Suspect 中的大对象和线程栈。

如果看到请求线程、Hutool JSON、BeanToMapCopier,继续展开。

步骤 3:看 Dominator Tree

按 Retained Heap 排序,重点看:

java.lang.Object[]
java.util.ArrayList
java.lang.Byte
cn.hutool.json.JSONArray
cn.hutool.json.JSONObject

不要只盯单个 Byte,应优先找承载它们的 Object[]、ArrayList、JSONArray。

步骤 4:看 Incoming References

对大对象右键:

List Objects -> with incoming references

找到是谁持有该对象。

步骤 5:看 Outgoing References

如果发现上游是:

BeanToMapCopier

则对它右键:

List Objects -> with outgoing references

重点查看:

source
target
targetType
copyOptions

如果 source 是:

com.tongweb.coyote.Request
com.tongweb.catalina.connector.Request
HttpServletRequest
ServletRequest

即可确认是请求对象被序列化。

步骤 6:回源码搜索触发点

搜索:

JSONUtil.toJsonStr
new JSONObject
JSONUtil.parseObj
BeanUtil.beanToMap
joinPoint.getArgs

本次最终定位到:

JSONUtil.toJsonStr(joinPoint.getArgs())

7. 根因结论

本次 OOM 的直接原因是:

操作日志切面在记录请求参数时,直接序列化 joinPoint.getArgs()。
当 Controller 方法参数中包含 HttpServletRequest 时,Hutool 会递归展开 TongWeb Request 对象。
Request 内部包含 headers、parameters、inputBuffer、cookies、response、socket wrapper 等复杂结构。
Hutool 将其中部分字节数据展开成大量 Byte 对象和 JSONArray/ArrayList,最终导致 Java heap space。

简化链路:

LogAspect.setRequestValue
-> joinPoint.getArgs()
-> args 包含 HttpServletRequest
-> JSONUtil.toJsonStr(args)
-> BeanToMapCopier
-> source = com.tongweb.coyote.Request
-> target = JSONObject
-> JSONArray / ArrayList / Byte 大量创建
-> Java heap space

8. 修复

方案 1:过滤不可序列化的 Servlet 对象

private void setRequestValue(JoinPoint joinPoint, OperLog operLog) {
Map<String, String[]> paramsMap = httpServletRequest.getParameterMap();
if (!paramsMap.isEmpty()) {
operLog.setOperParam(JSONUtil.toJsonStr(paramsMap));
return;
}

Object[] args = Arrays.stream(joinPoint.getArgs())
.filter(this::isLoggableArg)
.toArray();
operLog.setOperParam(JSONUtil.toJsonStr(args));
}

private boolean isLoggableArg(Object arg) {
return arg != null
&& !(arg instanceof ServletRequest)
&& !(arg instanceof ServletResponse)
&& !(arg instanceof MultipartFile)
&& !(arg instanceof InputStream)
&& !(arg instanceof OutputStream);
}

赞(0)
未经允许不得转载:171主机测评 » OOM 排查复盘:Hutool 序列化 Request 导致 Java Heap Space
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址