MyBatis(解决内存溢出问题)
在上周进行MongoDB的POC验证中,为了初始化数据,写了一段代码将MySQL数据库的复制n份并copy到MongoDB中。在这过程中使用了MyBatis访问MySQL,由于循环查询数据库数据,所以每次查询完数据后并没有关闭SqlSession,造成了内存泄露。
程序片段代码如下,然后实际中启用5个线程同时执行。
- public void copyReply2MongoDB() {
- MongoDao mongoDao = new MongoDaoImpl();
- SqlSession sqlSession = null;
- List<Map<String, Object>> replyList = null;
- MySQLDao dao = null;
- try {
- sqlSession = DBUtil.getSqlSessionFactory().openSession();
- for (int i = 0; i < 125; i++) {
- dao = sqlSession.getMapper(MySQLDao.class);
- Map<String, Object> paramMap = new HashMap<String, Object>();
- paramMap.put("offset", i * BATCH_SIZE);
- paramMap.put("limit", BATCH_SIZE);
- replyList = dao.queryReply(paramMap);
- LOGGER.info(getName() + " begin insert, i = " + i);
- long start = System.currentTimeMillis();
- for (Map<String, Object> reply : replyList) {
- DBObject object = new BasicDBObject();
- for (Map.Entry<String, Object> entry : reply.entrySet()) {
- Object value = entry.getValue();
- if (value != null) {
- object.put(entry.getKey(), value);
- }
- }
- mongoDao.insertComments(object);
- }
- long end = System.currentTimeMillis();
- LOGGER.info(getName() + " copyReply2MongoDB, i = " + i
- + ", insert time = " + (end - start));
- }
- } finally {
- sqlSession.close();
- }
- }
程序运行现象
将程序打成poc.jar包,然后在一台ubuntu linux机器上执行: java -Xms1024m -Xmx1024m -jar poc.jar
在执行过程中,开始运行一切良好,等循环到90次以后,使用mongostat监控到插入记录的速度越来越慢,从每秒钟插入上千条记录到个位数,打印的insert time也越来越慢,top命令看见java进程的cpu使用率从百分之几十稳定在100%左右,也就是把一个核心的cpu基本占用完毕。执行kill -3 javapid,从thread dump信息看,线程之间并没有出现死锁,而最后一行的堆信息看,堆基本已经被占满了,怀疑程序出现了内存泄露,100%的CPU现象是gc造成的。
验证存在内存泄露
为了验证程序是否真的存在,在本地运行poc.jar,使用jvisualvm 的visual gc插件 进行监控,最开始插入速度一切正常,等老代被占满了之后,插入速度明显变慢,程序基本都是在执行gc。截图如下:
从图中可以清晰的看出,在3:50分之后,gc的频率加快了非常多,而且cpu使用率也增加非常高,此时堆中old区域基本100%占用。
猜想有问题的代码
由于程序很简单,最开始觉得发生内存泄露的地方可能是循环外的这两行代码,
List<Map<String, Object>> replyList = null;
MySQLDao dao = null;
因为对于java的memory leak基本就是长生命周期的对象大量持有了很多其他短生命周期的对象,使得短生命周期的对象不能够及时的回收掉,而容器类出现这种的概率最大。将上述2行代码移动到循环内,结果发现没有任何作用,现象依旧。怎么办呢,那就只能把堆dump出来,看到底是什么对象占用了大量的内存。
dump heap 分析内存
直接用jvisualvm dump当前java的堆,使用MemoryAnalyzer打开当前的heap,然后使用Leak Suspects功能打开当前的heap,截图如下

然后点开Details,可以看见当前的对象数量和占用的内存大小
从图中可以看出,MySQLBO中的DefaultSqlSession占用了大量的内存,而DefaultSqlSession是从哪里来的呢,翻开MyBatis的源码,原来SqlSessionFactory获取一个session的时候,是来至于DefaultSqlSession,那导致内存泄露的原因基本上就找到了。修改就很好修改了,每次循环开始的时候openSession,完毕执行sqlSession.close()就可以了。
后记
最开始执行程序发现有问题,到最解决这个问题,也花了几个小时的时间,最开始并没有怀疑到是MyBatis上,以为是自己代码的问题。第一次程序执行越来越慢的时候也没有去看到底是啥问题,等第二次执行出现了同样的现象才开始关注,也不会想到是gc问题,还好执行kill -3的时候thread dump里面有堆的当前内存信息,否则根本想不到是gc问题。而且即使长时间堆基本被占满,也没有出现OOM,这个倒是以前少见的(触发OOM的条件没有达到)。