一、现象描述
一个JVM进程持续进行Full GC,老年代堆内存还是占用超过90%以上。
二、排查复盘
JVM进程内存得不到释放,一般有两个原因:1)内存中的对象的确仍处于存活状态;2)存在内存泄漏。
排查过程复盘:
- 分别执行
jmap
和jstack
命令,dump堆内存快照和线程快照 - 使用Memory Analyzer解析堆内存快照,根据“Leak_Suspects”文件报告,显示存在两个“疑似问题点”,如图1所示
- 打开“疑似问题1”的详细信息,部分截图如图2和3所示,显示一个MultiJedisPool实例对象中的HashMap成员对象(结合源代码,可定位到该成员字段名为“jedisPoolMap”)占用大约38%的内存,该HashMap对象保存的键值对为
<Jedis对象,该Jedis对象所属JedisPool在一个JedisPool列表中的下标索引>
,当一个Jedis被客户端使用时,创建一条Entry记录;当一个Jedis被客户端归还时,删除相应的Entry记录。根据图3,显示该HashMap对象拥有多达327736条Entry记录,而根据我们的JedisPool(底层是Apache的“commons-pool”库)配置,JedisPool理论上不可能创建这么多不同的Jedis对象,这个应该是先前已经被发现的Apache的“commons-pool”库的bug造成的,该版本(1.5.4)的“commons-pool”库不仅会创建超过配置数量的Jedis对象,而且还可能将一个未被归还的Jedis对象重新分配出去。不过,即便存在创建过多Jedis对象的bug,如果正确归还Jedis对象并清除相应Entry记录的话,上述HashMap对象也不至于持有如此巨大数量的Entry记录,查看相应归还Jedis对象的源代码,发现果然存在一个内存泄漏点,具体源代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15synchronized public void returnRedisClient(Jedis jedis) {
if (jedis == null)
return;
Integer listIdx = jedisPoolMap.get(jedis);
if (listIdx == null) {
for (JedisPool jedisPool : jedisPoolList)
jedisPool.returnResource(jedis);
} else {
// 正确的应该是“jedisPoolMap.remove(jedis)”,导致内存泄漏
jedisPoolMap.remove(listIdx);
jedisPoolList.get(listIdx).returnResource(jedis);
}
} - 打开“疑似问题2”的详细信息,部分截图如图4所示,显示同样是一个MultiJedisPool实例对象中的HashMap成员对象占据过大内存,即“疑似问题2”与“疑似问题1”的问题原因相同
图1
图2
图3
图4
三、其他
另外一个服务节点上有个同应用JVM进程出现了同样的现象,经过排查,发现原因也是一致的,它甚至导致了高达69%的内存占用,如图5所示。
图5