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 小时前
Agent入门阶段-编程基础-Python:流程控制
python·agent·ai编程
用户128526116023 小时前
我把祖传Java项目重构后,接口响应从3s砍到了200ms,只改了这几行代码
java
Linsk3 小时前
组件 = 模板 + 业务逻辑
java·前端·vue.js
呱呱复呱呱3 小时前
Django CBV 源码解读:一个请求是怎么找到你的 get() 方法的
python·django
星沉远浦4 小时前
用Gemini高效解决Java代码报错难以定位的问题
java
用户298698530148 小时前
Word 文档字符级格式化:Java 实现方案详解
java·后端
曲幽8 小时前
刚部署的 LibreTranslate 频频翻车?我掏出了 20 年前的 StarDict 词典,用 FastAPI 搭了个本地词典翻译 API
python·fastapi·web·translate·goldendict·libretranslate·stardict·pystardict
笨鸟飞不快8 小时前
从单个服务到集群:一次完整的性能排查复盘
java·前端
荣码8 小时前
用Streamlit给AI应用套个界面,10行代码出Web页面
java·python
SamDeepThinking8 小时前
Java微服务练习方式
java·后端·微服务