解决MyBatis的SqlSession不及时close导致的内存泄露一例

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

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

public void copyReply2MongoDB() {
		MongoDao mongoDao = new MongoDaoImpl();
		SqlSession sqlSession = null;
		List> replyList = null;
		MySQLDao dao = null;
		try {
			sqlSession = DBUtil.getSqlSessionFactory().openSession();
			for (int i = 0; i < 125; i++) {
				dao = sqlSession.getMapper(MySQLDao.class);
				Map paramMap = new HashMap();
				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 reply : replyList) {

					DBObject object = new BasicDBObject();
					for (Map.Entry 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> 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的条件没有达到)。


你可能感兴趣的:(解决MyBatis的SqlSession不及时close导致的内存泄露一例)