MyBatis(解决内存溢出问题)

MyBatis(解决内存溢出问题)



在上周进行MongoDB的POC验证中,为了初始化数据,写了一段代码将MySQL数据库的复制n份并copy到MongoDB中。在这过程中使用了MyBatis访问MySQL,由于循环查询数据库数据,所以每次查询完数据后并没有关闭SqlSession,造成了内存泄露。

程序片段代码如下,然后实际中启用5个线程同时执行。

  1. public void copyReply2MongoDB() {  
  2.         MongoDao mongoDao = new MongoDaoImpl();  
  3.         SqlSession sqlSession = null;  
  4.         List<Map<String, Object>> replyList = null;  
  5.         MySQLDao dao = null;  
  6.         try {  
  7.             sqlSession = DBUtil.getSqlSessionFactory().openSession();  
  8.             for (int i = 0; i < 125; i++) {  
  9.                 dao = sqlSession.getMapper(MySQLDao.class);  
  10.                 Map<String, Object> paramMap = new HashMap<String, Object>();  
  11.                 paramMap.put("offset", i * BATCH_SIZE);  
  12.                 paramMap.put("limit", BATCH_SIZE);  
  13.                 replyList = dao.queryReply(paramMap);  
  14.                 LOGGER.info(getName() + " begin insert, i = " + i);  
  15.                 long start = System.currentTimeMillis();  
  16.                 for (Map<String, Object> reply : replyList) {  
  17.   
  18.                     DBObject object = new BasicDBObject();  
  19.                     for (Map.Entry<String, Object> entry : reply.entrySet()) {  
  20.                         Object value = entry.getValue();  
  21.                         if (value != null) {  
  22.                             object.put(entry.getKey(), value);  
  23.                         }  
  24.                     }  
  25.                     mongoDao.insertComments(object);  
  26.                 }  
  27.                 long end = System.currentTimeMillis();  
  28.                 LOGGER.info(getName() + " copyReply2MongoDB, i = " + i  
  29.                         + ", insert time = " + (end - start));  
  30.   
  31.             }  
  32.         } finally {  
  33.             sqlSession.close();  
  34.         }  
  35.     }  


程序运行现象

将程序打成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的条件没有达到)。