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

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

1. 问题现象

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

text 复制代码
java.lang.OutOfMemoryError: Java heap space

主要异常时间点:

text 复制代码
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

日志中多次出现接口:

text 复制代码
/xxxxx

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

text 复制代码
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. 涉及代码

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

java 复制代码
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 请求:

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

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

java 复制代码
httpServletRequest.getParameterMap()

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

java 复制代码
Object[] args = joinPoint.getArgs();
JSONUtil.toJsonStr(args);

args 中包含:

text 复制代码
LoginDto
HttpServletRequest

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

3. HPROF 分析结果

使用 heap dump:

text 复制代码
D:\xxxxx\doc\heap202606021028.hprof

对象统计中发现:

text 复制代码
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 或对象引用链中,找到可疑对象:

text 复制代码
cn.hutool.json.JSONObject @ 0xa004b4e0

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

text 复制代码
target cn.hutool.core.bean.copier.BeanToMapCopier

再对 BeanToMapCopier 查看:

text 复制代码
List Objects -> with outgoing references

看到关键字段:

text 复制代码
source com.tongweb.coyote.Request
target cn.hutool.json.JSONObject
targetType class cn.hutool.json.JSONObject
copyOptions cn.hutool.core.bean.copier.CopyOptions

这就是最关键证据。

它说明 Hutool 正在执行:

text 复制代码
com.tongweb.coyote.Request -> cn.hutool.json.JSONObject

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

这与 LogAspect 中的:

java 复制代码
JSONUtil.toJsonStr(joinPoint.getArgs())

完全吻合。

5. Incoming References 和 Outgoing References 怎么看

Incoming References

含义:

text 复制代码
谁引用了当前对象

用于回答:

text 复制代码
这个对象为什么还没有被 GC?
是谁持有了它?

例如:

text 复制代码
JSONObject <- BeanToMapCopier.target <- Java Local TaskThread

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

Outgoing References

含义:

text 复制代码
当前对象引用了谁

用于回答:

text 复制代码
这个对象内部装了什么?
它正在处理哪个 source?

例如对 BeanToMapCopier 查看 outgoing references:

text 复制代码
source com.tongweb.coyote.Request
target cn.hutool.json.JSONObject
copyOptions ...

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

6. 标准排查步骤

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

搜索:

text 复制代码
OutOfMemoryError
Java heap space
JSONUtil
JSONArray
JSONObject
BeanToMapCopier

如果看到:

text 复制代码
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 排序,重点看:

text 复制代码
java.lang.Object[]
java.util.ArrayList
java.lang.Byte
cn.hutool.json.JSONArray
cn.hutool.json.JSONObject

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

步骤 4:看 Incoming References

对大对象右键:

text 复制代码
List Objects -> with incoming references

找到是谁持有该对象。

步骤 5:看 Outgoing References

如果发现上游是:

text 复制代码
BeanToMapCopier

则对它右键:

text 复制代码
List Objects -> with outgoing references

重点查看:

text 复制代码
source
target
targetType
copyOptions

如果 source 是:

text 复制代码
com.tongweb.coyote.Request
com.tongweb.catalina.connector.Request
HttpServletRequest
ServletRequest

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

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

搜索:

text 复制代码
JSONUtil.toJsonStr
new JSONObject
JSONUtil.parseObj
BeanUtil.beanToMap
joinPoint.getArgs

本次最终定位到:

java 复制代码
JSONUtil.toJsonStr(joinPoint.getArgs())

7. 根因结论

本次 OOM 的直接原因是:

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

简化链路:

text 复制代码
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 对象

java 复制代码
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);
}
相关推荐
列星随旋1 小时前
矩阵快速幂
java·算法·矩阵
闪电悠米1 小时前
黑马点评-分布式锁-02_simple_redis_lock_setnx
java·数据库·spring boot·redis·分布式·缓存·wpf
gf13211111 小时前
【精确查找python脚本是否在运行】
linux·前端·python
zhangfeng11331 小时前
DeepSeek V4 适配华为昇腾950 难度及开源情况
人工智能·pytorch·python·机器学习·华为·开源
萨小耶1 小时前
[Java学习日记10】聊聊checked exception和runtime exception
java·开发语言·学习
超梦dasgg1 小时前
IDEA(IntelliJ IDEA)超详细基础使用教程
java·ide·intellij-idea
404号扳手1 小时前
Java 进阶知识(八)
java·后端
MU在掘金916951 小时前
给AI Agent做一个代码大脑:我用Tree-sitter+ChromaDB+MCP搭了个代码知识库
git·python
噜噜噜阿鲁~1 小时前
python学习笔记 | 11.5、面向对象高级编程-使用枚举类
笔记·python·学习