0%

一个JVM进程老年代堆内存不能释放问题

一、现象描述

一个JVM进程持续进行Full GC,老年代堆内存还是占用超过90%以上。

二、排查复盘

JVM进程内存得不到释放,一般有两个原因:1)内存中的对象的确仍处于存活状态;2)存在内存泄漏。
排查过程复盘:

  • 分别执行jmapjstack命令,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
    15
    synchronized 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

您的支持将鼓励我继续分享!