前段时间生产系统突然假死,重启后排查问题时发现是内存溢出(OutOfMemoryError: Java heap space),还好有配置自动生成快照文件。之前只有本地用玩具代码模拟内存泄露或者是那些一眼就能看出来的问题,所以特意把这次的排查过程记录下来。项目使用的springcloud。
-
先把快照文件下到本地,然后用Jprofiler工具打开快照文件,看到如下图
1.png - 图中
char[]
数组非常大,占据了总大小的65%左右。因此右击char[]
,选中Use Selected Objects
,进入到Biggest Objects
界面,如下图
2.png - 配合着
Jdk的bin目录下的jvisualvm.exe
工具,发现这个看起来像FeignClients调用接口的返回结果有5000多个且都是131KB大小,占据了char[]
数组大小的90%,并且发现里面有一个风格的报文占了4000多个。于是开始分析该报文的引用链,打开References
界面,切换成Incoming references
,如下图
3.png - 可以看到这个对象是放在
ThreadLocal
对象中的,这个放入的操作来自于线程池创建的线程,并且所有这些线程的状态都处于waiting,但是又看不出来这个ThreadLocal
对象是属于哪个类下面的。于是分析项目源码,发现项目中并没有显示的把接口返回结果放入到ThreadLocal
中去,当时真的傻眼了...这看源码都没头绪。浪费了好长一段时间,突然想到可以使用IDEA强大的Debug功能来帮助我找到这个放入到ThreadLocal
的操作是谁干的,IDEA可以直接在源码上调试断点。于是我使用本地环境,在ThreadLocal
的set方法上加了条件断点,最终找到了调用链,如下图
4.png - 开始分析源码,发现FeignClients在解析返回结果时使用了
Fastjson
工具包,Fastjson
的一个优点是解析速度非常快,使用ThreadLocal
存放序列化后的char[]
数组,避免重复分配内存空间。然后又看了下FeignClients使用Fastjson
的地方,源码显示是将接口返回的流解析成相应的对象。源码如下图
5.png
6.png - 现在找到了
ThreadLocal
和接口响应结果的关联关系,但是还没找到为什么会内存泄露的原因。一开始看到ThreadLocal
时第一反应是ThreadLocal
的null key导致的内存泄露问题,但是看源码时发现Jdk1.8已经在set等操作时将key为null的Entry对象过滤了一遍,使其可以被回收,避免了该问题。于是看那个异常4000次的接口的所有调用场景,每次调用接口都创建一个新的线程池,然后提交调用接口的业务逻辑job。于是我写了一个玩具代码来模拟项目中的这个数据流转场景,发现这种写法使用Fastjson
时,并且主动让线程等待可以复现生产上的这个现象,因为线程没结束,所以放在ThreadLocal
中的value自然无法被回收。如下图玩具代码示例
7.png
现在问题找到了,把线程池的写法改一下就行。
但是为什么这样写会导致线程池创建的线程处于waiting而不结束的原因还没理的明白...跪求大佬能留言解释一下
根据测试结果得到的线程处于waiting的原因是因为创建的线程池的核心线程数为1,而线程池的设计的是如果运行的任务小于核心线程数时会调用阻塞队列的take()
阻塞方法,于是导致线程处于waiting而无法被回收,因此当前线程的ThreadLocal.ThreadLocalMap.Entry
对象无法被回收。
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take(); <——此处调用队列的阻塞方法
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
网友评论