您的位置:首页 > 编程语言 > Java开发

关于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。

最后要说明的是,本文并不是说组件有问题,而是想说明,不合理的使用可能会导致问题。希望能对大家有所帮助,同时也欢迎大家的指正。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  OOM 内存泄露 java
相关文章推荐