诊断 ThreadLocal 引起的内存泄漏

今天遇到一起由于 ThreadLocal 没有正确使用引起的内存泄漏, 诊断过程中有些推断步骤还是有值得借鉴的意义的. 1: 如何快速找到当前线程中占用最大内存的对象; 2: 如何找到 ThreadLocal 变量的代码点.

症状描述

某个应用经过一周的逐渐积累, 导致heap空间不足, 引起OOM. 于是找个server 做了heap dump, 然后全部重启, 应用自动恢复. 分析获得的 heap dump, MAT 给出的结果是 437 个线程占用了 87% (1.5G) 内存. 如下图:
heap.png

从图中可以看出, MAT 找到这437个线程肯定存在某些共同点, 从上面列举的这些线程的名字看, 它们都有同样的模式, 那么仅仅告诉我们这些线程, 我们该如何找到这些线程那些对象占用了很多heap 空间呢?

如何找到某个线程 Context 中占用大量heap的对象?

有时候 MAT 会直接告诉我们是那个实例对象占用了大量内存, 在哪个线程运行 Context 里面. 我们大概可以通过类名, 或者即便多翻翻正在运行的栈里的变量, 都能顺利的找到.
今天我们顺着给出的线程名去翻当前栈帧, 却没有那么容易找到, 如下图:
stack.png

这个栈帧很常见, 表示这个线程是一个worker, 它正在等待任务队列里面能执行的任务, 说明它现在正在等待任务. 点开它的栈帧其实都是线程池里面的一些对象, 没有我们要找的大对象, 但是右边的 Retained Heap 列明显告诉我们它占了45M多多内存. 如何找到这个占用较多内存的对象呢?

其实点开这个栈帧的其实位置 java.lang.Thread.run(), 就可以看到当前线程引用的所有对象. 里面包括栈上方法参数, 局部变量, lamda 表达式变量以及 ThreadLocal 变量. 如下图:
var.png

从这里可以看到这个线程所引用的所有变量, 右边的 Retained Heap 能看到占用的heap的大小.

查看 ThreadLocal 的变量内容

点开 ThreadLocal 变量的 ThreadLocalMap, 我们能看到每个 entry, 根据右边 retained heap 列, 我们能找到我们要找的对象. 这个例子里我们看到它的 value 是一个 HashMap.
更为关键的是: 我们可以看到引用这个 ThreadLocal 对象的变量, 也就是它在哪个类里面定义的 referent. 本例中可以看到它是一个 ThreadLocalSvcCache$1. 基本根据这个提示就能找到这个 ThreadLocal 的定义代码处.

ThreadLocal 里面存放的内容和线程的生命周期

假如 ThreadLocal 里面存放的只是计数类的内容, 即使这个线程的生命周期再长, 占用的内存基本也是固定的.
假如 ThreadLocal 里面存放的内容是持续增长的, 就要注意什么时候要清除或者整理, 否则这个内容迟早会把内存搞爆.
上面这个例子中就是第二种情况, 只不过有些线程它清理了, 有些线程忘记了清理这些ThreadLocal 变量的内容.

标签: none

添加新评论