关于DB组件造成的内存泄露问题解析及解决
2017-02-14 09:56
896 查看
系统最近新上一个版本,跑全量初始化时,一段时间后出现OOM异常。当时我们第一反应是排查本版本相对上一版本的改动点,以及实现方式是否有问题。
经业务场景分析,全量计算方式相对上一版本,总体思路没有太大变化,唯一变化的是,最终汇总的计算结果多了两个。其中一个结果数据量较大。
看到这里,我们第一反应是存储结果数据的list没有及时GC导致内存泄露,因此我们检查相应的list,看看有没有主动clear操作,以提高GC效率。然而我们每个使用大list的地方,都是有主动clear操作的,问题排查陷入僵局。
后 来我们对比分析javaCore文件和dump文件,以及服务器GC日志。这里要说明一下,我们系统所部署的服务器是IBM WebSphere。分析工具,javaCore文件用的是jca452.jar(IBM Thread and Monitor Dump Analyzer for Java),dump文件用的是ha426.jar(IBM HeapAnalyzer),GC日志分析使用ga16.jar(IBM Pattern Modeling and Analysis Tool for Java Garbage
Collector)。
分析javaCore,发现出问题的时候,数据库等待很多,如下图1、2所示:
图1
图2
因此怀疑问题出在和数据库相关的操作上。我们也曾找过dba分析当时的数据库情况,连接数和系统负载等都良好。看来问题还是我们掉用方这边的问题。
继续看dump文件,发现如下可疑信息:
图3
其中,ShardingDalClient是公司操作数据库的组件,NamedParameterJdbcTemplate是Spring封装的数据库操作组件。我们不太相信组件是有问题的,于是又分析了下服务器的GC:
图4
从23点34分开始运行全量计算,系统的内存消耗持续增加,而GC虽然也在进行,却赶不上消耗的速度。
不得已,再次抱着怀疑的态度阅读组件源码。
这 里先提前交代下我们全量计算入库语句的实现方式。因为计算结果数据量较大,入库耗时较多,我们使用多values方式入库: insert into tablename (colum1,…,columN) values (…),(…),…,(…) 。即,将多个insert拼接成一条语句执行。于是我们的SQLMap中的SQL语句标签是这样的:
图5
在代码中,将待入库的数据拼接成一个(…),(…)这样的长串,然后作为values参数。
接下来看我们数据库组件的execute方法,发现可疑处execute4PrimaryKey方法:
图6
此方法根据sqlId和入参,解析获得一个sql。这里我们再看下getBoundSql方法都做了什么:
图7
类 名和方法都很直白:解析FreeMaker标签。再回顾我们的sql标签设计,有两个${},这个就是FreeMaker标签,组件会先解析,将${}部 分替换掉,再继续往下走。到这里,估计很多人都可能猜到问题原因了:每次大量的不同的values数据,虽然句式一样,单却会生成不同sql。生成的不同 sql有啥问题呢?继续跟代码,看图6方法中的update放法。注意,此时的入参,是一个很大的sql,而不是原始的sql标签了。Update方法如 下图:
图8
此时的update方法已经属于Spring组件的逻辑了。
关注getParsedSql方法:
图9
我们看看方法都干了什么:从parsedSqlCache中,以sql作为key获取对应的ParsedSql,取不到,则新解析一个,并put进parsedSqlCache中。
这里,parsedSqlCache是一个LinkedHashMap,规模256,遵循LRU算法(最近最少使用)。说白了就是,这个map最多只存256个元素,超过时,会将最少使用的remove掉。
这 些都不是重点,重点是this.parsedSqlCache.put(sql, parsedSql); 这句,此时的sql是很大的!最坏情况下,会存256份超大的sql,而且这些sql是由parsedSqlCache持有,因此GC也没法回收它们(至 少在被新的sql顶替掉之前没法回收)。这也印证了dump文件分析截图中,那个LinkedHashMap持有内存较大的现象。
于是为了解决此问题,我们从两方面着手:一方面调整sql标签方式,计算结果较大的数据,使用batchUPdate方式入库,另一方面减少批量insert的规模。
调整后的sql标签如下:
图10
表 头部分的数量是可控的,并且分库分表的实现就靠它了,因此不改。重点是values部分,冒号加变量,不会作为FreeMaker标签,因此,该SQL标 签走到getParsedSql方法查缓存,以及缓存补偿处,都不会变化。就算插入的数据量变化,都不影响parsedSqlCache中的值,实现了 sql语句预编译的效果,最重要的是避免parsedSqlCache中存储过大的SQL字符串。
批次数据入库数量缩小为200。经这两方面的调整后,问题解决,GC情况如下图示:
图11
本次修改并没有彻底将所有的多values方式改为batchUpdate方式,是考虑到,多values方式入库的效率会比batchUpdate方式高,这个着实令我们难以取舍,最终采取折中的方式,先改一部分并且限制批量数目。
细心的同学会发现,getParsedSql方法中有一个if (getCacheLimit() <= 0)的判断,只要我们设置cacheLimit小于或者等于0,就不会预存sql了。所以我想,我们应该可以实现不保存预编译的方式来执行sql。
最后要说明的是,本文并不是说组件有问题,而是想说明,不合理的使用可能会导致问题。希望能对大家有所帮助,同时也欢迎大家的指正。
经业务场景分析,全量计算方式相对上一版本,总体思路没有太大变化,唯一变化的是,最终汇总的计算结果多了两个。其中一个结果数据量较大。
看到这里,我们第一反应是存储结果数据的list没有及时GC导致内存泄露,因此我们检查相应的list,看看有没有主动clear操作,以提高GC效率。然而我们每个使用大list的地方,都是有主动clear操作的,问题排查陷入僵局。
后 来我们对比分析javaCore文件和dump文件,以及服务器GC日志。这里要说明一下,我们系统所部署的服务器是IBM WebSphere。分析工具,javaCore文件用的是jca452.jar(IBM Thread and Monitor Dump Analyzer for Java),dump文件用的是ha426.jar(IBM HeapAnalyzer),GC日志分析使用ga16.jar(IBM Pattern Modeling and Analysis Tool for Java Garbage
Collector)。
分析javaCore,发现出问题的时候,数据库等待很多,如下图1、2所示:
图1
图2
因此怀疑问题出在和数据库相关的操作上。我们也曾找过dba分析当时的数据库情况,连接数和系统负载等都良好。看来问题还是我们掉用方这边的问题。
继续看dump文件,发现如下可疑信息:
图3
其中,ShardingDalClient是公司操作数据库的组件,NamedParameterJdbcTemplate是Spring封装的数据库操作组件。我们不太相信组件是有问题的,于是又分析了下服务器的GC:
图4
从23点34分开始运行全量计算,系统的内存消耗持续增加,而GC虽然也在进行,却赶不上消耗的速度。
不得已,再次抱着怀疑的态度阅读组件源码。
这 里先提前交代下我们全量计算入库语句的实现方式。因为计算结果数据量较大,入库耗时较多,我们使用多values方式入库: insert into tablename (colum1,…,columN) values (…),(…),…,(…) 。即,将多个insert拼接成一条语句执行。于是我们的SQLMap中的SQL语句标签是这样的:
图5
在代码中,将待入库的数据拼接成一个(…),(…)这样的长串,然后作为values参数。
接下来看我们数据库组件的execute方法,发现可疑处execute4PrimaryKey方法:
图6
此方法根据sqlId和入参,解析获得一个sql。这里我们再看下getBoundSql方法都做了什么:
图7
类 名和方法都很直白:解析FreeMaker标签。再回顾我们的sql标签设计,有两个${},这个就是FreeMaker标签,组件会先解析,将${}部 分替换掉,再继续往下走。到这里,估计很多人都可能猜到问题原因了:每次大量的不同的values数据,虽然句式一样,单却会生成不同sql。生成的不同 sql有啥问题呢?继续跟代码,看图6方法中的update放法。注意,此时的入参,是一个很大的sql,而不是原始的sql标签了。Update方法如 下图:
图8
此时的update方法已经属于Spring组件的逻辑了。
关注getParsedSql方法:
图9
我们看看方法都干了什么:从parsedSqlCache中,以sql作为key获取对应的ParsedSql,取不到,则新解析一个,并put进parsedSqlCache中。
这里,parsedSqlCache是一个LinkedHashMap,规模256,遵循LRU算法(最近最少使用)。说白了就是,这个map最多只存256个元素,超过时,会将最少使用的remove掉。
这 些都不是重点,重点是this.parsedSqlCache.put(sql, parsedSql); 这句,此时的sql是很大的!最坏情况下,会存256份超大的sql,而且这些sql是由parsedSqlCache持有,因此GC也没法回收它们(至 少在被新的sql顶替掉之前没法回收)。这也印证了dump文件分析截图中,那个LinkedHashMap持有内存较大的现象。
于是为了解决此问题,我们从两方面着手:一方面调整sql标签方式,计算结果较大的数据,使用batchUPdate方式入库,另一方面减少批量insert的规模。
调整后的sql标签如下:
图10
表 头部分的数量是可控的,并且分库分表的实现就靠它了,因此不改。重点是values部分,冒号加变量,不会作为FreeMaker标签,因此,该SQL标 签走到getParsedSql方法查缓存,以及缓存补偿处,都不会变化。就算插入的数据量变化,都不影响parsedSqlCache中的值,实现了 sql语句预编译的效果,最重要的是避免parsedSqlCache中存储过大的SQL字符串。
批次数据入库数量缩小为200。经这两方面的调整后,问题解决,GC情况如下图示:
图11
本次修改并没有彻底将所有的多values方式改为batchUpdate方式,是考虑到,多values方式入库的效率会比batchUpdate方式高,这个着实令我们难以取舍,最终采取折中的方式,先改一部分并且限制批量数目。
细心的同学会发现,getParsedSql方法中有一个if (getCacheLimit() <= 0)的判断,只要我们设置cacheLimit小于或者等于0,就不会预存sql了。所以我想,我们应该可以实现不保存预编译的方式来执行sql。
最后要说明的是,本文并不是说组件有问题,而是想说明,不合理的使用可能会导致问题。希望能对大家有所帮助,同时也欢迎大家的指正。
相关文章推荐
- 多线程 DLL 回调函数问题,当应用程序退出时,有些操作并未完成,造成程序内存泄露,如何解决呢
- QuickReport报表Prepare之后造成内存泄露问题的解决方法
- 关于使用json库造成的内存泄露问题
- 关于多态里父类的析构函数造成子类内存泄露的问题
- 关于Android中使用Handler造成内存泄露的分析和解决
- 解决Android使用Handler造成内存泄露问题
- Android使用Handler造成的内存泄露问题的解决
- tomcat中使用Quartz造成内存泄露的问题解决
- VS2008中关于“加载安装组件时遇到问题。取消安装”的解决
- memset导致的内存泄露问题的解决办法
- 转贴:关于数据库对象所有者非dbo时的可能造成的问题及解决方法
- JQuery Dialog的内存泄露问题解决方法
- VS2008中关于“加载安装组件时遇到问题。取消安装”的解决
- VS2008中关于“加载安装组件时遇到问题。取消安装”的解决
- 一次GTK程序内存泄露的解决过程发现的两个内存泄露的问题
- 解决Java内存泄露问题方案
- 关于JavaScript解析XML的性能的问题(已解决)
- 关于EXTJS 2.2.1版本在IE环境下Grid组件表头下拉菜单图标错位问题的解决
- 关于Visual Studio .NET 2003 安装时“系统组件不匹配”问题的解决
- 【转啊转的啊】]关于“Web 创作组件” 解决MS Office 2007找不到Office.zh-cn问题以及VS2008 SP1 安装失败需指定visualwebdeveloperww.msi所在路径