记一次读取文件后ibatis执行SQL时的OOM异常

问题背景

编写工具解析文件包及文件内容,获取SQL文件并根据配置数据库连接进行更新,涉及到文件流读取和mybatis.SqlSession等。在编码完成并且在win验证后,找了台Linux环境进行自测,在运行时报错

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:448)
at java.lang.StringBuilder.append(StringBuilder.java:136)
at org.apache.ibatis.logging.LogFactory.getLog(LogFactory.java:54)
at org.apache.ibatis.mapping.MappedStatement.Builder.(MappedStatement.java:81)
at org.apache.ibatis.session.defaults.DefaultSqlSession.getMappedStatement(DefaultSqlSession.java:409)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:254)
at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:247)
at sun.reflect.GeneratedMethodAccessor31.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.mybatis.spring.SqlSessionTemplate.SqlSessionInterceptor.invoke(SqlSessionTemplate.java:466)
at com.sun.proxy.$Proxy45.update(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:314)

  • 本机win是16G内存,报错的linux是8G
  • 脚本启动jvm时未指定内存大小,未指定gc
  • sql文件个数在几十不等,每个文件在kb至mb(个位数)不等

过程

之前没接触过此类问题,只能边查边学习边分析。

看日志是在ibatis的logfactory.getlog中,stringbuilder在进行扩容的时候发生内存溢出。不确定的是什么内容占用了如此大的内存,因为本身文件大小不是很大。

JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指定,默认是物理内存的1/4.默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时, JVM会减少堆直到-Xms的最小限制.因此服务器一般设置-Xms,-Xmx相等以避免在每次GC 后调整堆的大小.
相关博客文档

free -m 查看linux内存大小是7822m,堆默认最大值为2000m不到,OOM是内存占用已经接近2G…在我指定如下堆大小参数后,运行是正常。

-Xmx3550m -Xms3550m

linux内存大小

结合visualVM和dump文件进行分析

  • 增加OOM时的dump文件

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/

  • visualvm监控运行时的内存变化

https://blog.csdn.net/weixin_42740530/article/details/98983229
jvisualvm

在拿到jprof文件后,可以通过visualvm,jprofiler或者eclipse的memory analyse等工具进行分析查看,字符串占用空间太大,有太多的实例化次数,占用了接近1.5G的内存。
char[]数组
memory analyse的问题分析
可能是文件流读取或者stringbuilder()的使用方式不对产生了大量冗余string且没有被回收?在比较多种实现方式后,目前bufferreader+stringbuilder的编写逻辑是没有问题的,是否在构造时候指定大小并没有影响。

排除法分析内存消耗较高的代码逻辑

  • 只保留读取文件的逻辑,内存占用在200m
  • 保留读取文件和解析文件SQL的逻辑,内存占用成波形,最高不到700m,可能和文件流的释放有关系?
  • 放开sqlsession.update逻辑执行SQL之后,内存占用很快飙升到接近2G,并产生OOM错误

ibatis执行时候为什么会需要这么大的内存呢?在前边分析的过程中已经查看源码进行了分析,怀疑和它的日志处理有关系。

    package org.apache.ibatis.session.defaults;
    public int update(String statement, Object parameter) {
        int var4;
        try {
            this.dirty = true;
            // mybatis的核心类,存储了sql的所有相关信息
            MappedStatement ms = this.getMappedStatement(statement, parameter == null ? Object.class : parameter.getClass());
            var4 = this.executor.update(ms, this.wrapCollection(parameter));
        } catch (Exception var8) {
            throw ExceptionFactory.wrapException("Error updating database.  Cause: " + var8, var8);
        } finally {
            ErrorContext.instance().reset();
        }

        return var4;
    }
    
    private MappedStatement getMappedStatement(String sql, Class<?> parameterType) {
        if (this.configuration.hasStatement(sql)) {
            return this.configuration.getMappedStatement(sql);
        } else {
            SqlSource sqlSource = new RawSqlSource(this.configuration, sql, parameterType);
            // 静态构造类
            Builder builder = new Builder(this.configuration, sql, sqlSource, SqlCommandType.UPDATE);
            return builder.build();
        }
    }
        public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
            this.mappedStatement.configuration = configuration;
            this.mappedStatement.id = id;
            this.mappedStatement.sqlSource = sqlSource;
            this.mappedStatement.statementType = StatementType.PREPARED;
            this.mappedStatement.resultSetType = ResultSetType.DEFAULT;
            this.mappedStatement.parameterMap = (new org.apache.ibatis.mapping.ParameterMap.Builder(configuration, "defaultParameterMap", (Class)null, new ArrayList())).build();
            this.mappedStatement.resultMaps = new ArrayList();
            this.mappedStatement.sqlCommandType = sqlCommandType;
            this.mappedStatement.keyGenerator = (KeyGenerator)(configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType) ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE);
            String logId = id;
            if (configuration.getLogPrefix() != null) {
                logId = configuration.getLogPrefix() + id;
            }
            // 获取当前sql的日志
            this.mappedStatement.statementLog = LogFactory.getLog(logId);
            this.mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
        }
    public static Log getLog(String logger) {
        try {
            // 获取当前日志实现类构造实例,slf4j
            return (Log)logConstructor.newInstance(logger);
        } catch (Throwable var2) {
            // 实际报错是这一行,catch到的也是内存溢出错误,在此处append字符串的时候再次报错内存溢出
            throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + var2, var2);
        }
    }
    static {
    	// 静态方法区,在启动时获取当前环境的日志实现,工具用到了slf4j
        tryImplementation(LogFactory::useSlf4jLogging);
        tryImplementation(LogFactory::useCommonsLogging);
        tryImplementation(LogFactory::useLog4J2Logging);
        tryImplementation(LogFactory::useLog4JLogging);
        tryImplementation(LogFactory::useJdkLogging);
        tryImplementation(LogFactory::useNoLogging);
    }

mybatis官网的日志说明
知道了ibatis的日志框架之后,大概率是在处理日志的时候占用了大量内存。在环境只记录程序日志不需要开启mybatis的debug模式时,可以先把日志关掉,在定义sqlsession的时候,指定日志的实现类

sqlSession.getConfiguration().setLogImpl(NoLoggingImpl.class);

再次在环境验证正常,内存占用又回到了之前的波形…ibatis的日志记录为什么会消耗这么大的内存呢?后续继续分析这块,有大佬也欢迎多指教。目前来看,先这样处理,在大多数的开发环境和生产配置也都是比较高的,理论上不会有这样的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值