日志组件导致的内存溢出问题分析

1、 内存溢出日志

普通的http请求,导致堆内存直接溢出,看了下代码实现非常简单的一次DB查询且数据量也比较小,不可能导致内存溢出呢

java.lang.OutOfMemoryError: Java heap space

at java.util.Arrays.copyOf(Arrays.java:3332)

at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)

at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:649)

at java.lang.StringBuffer.append(StringBuffer.java:387)

at java.io.StringWriter.write(StringWriter.java:77)

at java.io.StringWriter.append(StringWriter.java:202)

at java.io.StringWriter.append(StringWriter.java:41)

at com.google.gson.stream.JsonWriter.beforeValue(JsonWriter.java:645)

at com.google.gson.stream.JsonWriter.value(JsonWriter.java:532)

at com.google.gson.internal.bind.TypeAdapters$5.write(TypeAdapters.java:189)

at com.google.gson.internal.bind.TypeAdapters$5.write(TypeAdapters.java:173)

2、分析代码实现

排除代码实现的问题那一定是在代码执行前干了什么时间导致内存溢出,查看日志发现异常发生在 ParamLogAspect.java 中,代码实现如下

java 复制代码
@Before("execution(* com.example.controller..*(..))")
public void logBefore(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature)joinPoint.getSignature();
    String className = signature.getDeclaringType().getName();
    String methodName = signature.getName();
    Object[] args = joinPoint.getArgs();
    StringBuilder jsonArgsBuilder = new StringBuilder();
    try {
        jsonArgsBuilder.append(new Gson().toJson(arg));
    } catch (Throwable e1) {
        log.info("转换函数入参json失败 - {}", e1, args != null ? args.toString() : "");
    }
    log.info("[aop operation log] {}.{} , Params:{}", className, methodName, jsonArgsBuilder);
}

初看也没发现什么问题,断点发现卡在了 new Gson().toJson(arg) ,断点调试跟进发现入参类型为 HttpServletRequest ,难道它不可直接打印 ?

查看 HttpServletRequest 说明发现该类包含大量的内部状态和引用,比如输入流、会话信息、请求头等,直接序列化这些信息可能会形成循环引用或过于庞大,导致序列化过程占用过多内存甚至进入无限循环。为了避免这种情况,建议手动提取 HttpServletRequest 中的有用信息,并将这些信息序列化。

3、优化组件日志记录方式

将日志组件的实现改为参数部分初始化,再次请求就可以了

java 复制代码
@Before("execution(* com.example.controller..*(..))")
public void logBefore(JoinPoint joinPoint) {
    MethodSignature signature = (MethodSignature)joinPoint.getSignature();
    String className = signature.getDeclaringType().getName();
    String methodName = signature.getName();
    Object[] args = joinPoint.getArgs();
    StringBuilder jsonArgsBuilder = new StringBuilder();
    try {
        // 增加 HttpServletRequest 类型参数解析(HttpServletRequest 直接 toJson 会导致内存溢出)
        for (Object arg : args) {
            if(arg instanceof HttpServletRequest){
                HttpServletRequest request = (HttpServletRequest) args[0];
                Map<String, String> paramMap = new HashMap<>();
                Enumeration<String> parameterNames = request.getParameterNames();
                while (parameterNames.hasMoreElements()) {
                    String paramName = parameterNames.nextElement();
                    String paramValue = request.getParameter(paramName);
                    paramMap.put(paramName, paramValue);
                }
                jsonArgsBuilder.append(new Gson().toJson(paramMap)).append(" ");
            }
            else {
                jsonArgsBuilder.append(new Gson().toJson(arg)).append(" ");
            }
        }
    } catch (Throwable e1) {
        log.info("转换函数入参json失败 - {}", e1, args != null ? args.toString() : "");
    }
    log.info("[aop operation log] {}.{} , Params:{}", className, methodName, jsonArgsBuilder);
}

4、深度分析

什么原因导致的内存溢出呢,初步猜测可能导致的原因如下:

  • HttpServletRequest 对象内部存在循环引用,如:HttpSession、HttpServletRequest 和 ServletContext、ServletConfig 对象之间相互引用,导致 Gson 在处理这些循环引用时,可能会进入无限循环,导致内存溢出。
  • HttpServletRequest 内容及数量确实很大,占用大量内存,导致内存溢出
  • 非序列化字段或者动态生成的输入、输出流,在序列化过程中可能会引发异常或者占用大量内存。

从异常日志并没有发现有循环引用的调用栈,难道真是太大了导致的溢出?导出dump文件接着看下,还真是太大导致的

一个 request 占用了 89.37 %的内存,确实离谱了 !!!

相关推荐
骄马之死7 小时前
SpringMVC + SpringBoot 核心知识点总结
java·spring boot·后端
郑洁文9 小时前
基于Spring Boot的流浪动物救助网站
java·spring boot·后端·毕设·流浪动物救助
螺丝钉code9 小时前
JAVA项目 Claude code CLAUDE.md 到底应该怎么写
java·人工智能·claude code
摇滚侠11 小时前
Maven 入门+高深 单一架构案例 54-59
java·架构·maven·intellij-idea
VidDown11 小时前
Webhook 调试器:让第三方回调“原形毕露”
java·开发语言·javascript·编辑器·postman
折哥的程序人生 · 物流技术专研11 小时前
Java 23 种设计模式:从踩坑到精通 | 原型模式 —— 克隆对象,深拷贝与浅拷贝的坑你踩过吗?
java·设计模式·架构·原型模式·单一职责原则
装不满的克莱因瓶11 小时前
基于 OpenResty 扩展开发实现动态服务注册与发现能力
java·开发语言·架构·openresty
程序员小羊!11 小时前
06Java 异常机制与常用类
java
weixin_5231853212 小时前
Java基础知识总结(四):引用数据类型与参数传递机制
java·开发语言·python
宸津-代码粉碎机12 小时前
Spring AI企业级实战|从RAG优化到Agent多工具调度
java·大数据·人工智能·后端·python·spring