1、问题
某应用线上运行较长时间后出现Full GC告警,重启后恢复正常。
由于之前基本上半个月就会发一次版,导致问题一直没有暴露出来,这次较长时间没有对应用进行发版(或重启)。根据现象初步怀疑是由内存泄漏引起。
2、初步分析
拉取dump文件分析。发现线程池ConsumerQueryExecuteImpl和NoAccQb2cQueryExecuteImpl引用了大对象,引用的队列LinkedBlockingQueue和FutureTask都存在数量极大的Object,基本证实是由内存泄漏引起。
分析代码,发现ConsumerQueryExecuteImpl和NoAccQb2cQueryExecuteImpl都使用到线程池CompletionService。可以看出下图代码将多个查询操作放入线程池异步执行,通过Future的get方法获取执行结果,来做到进一步提高响应速度。
3、源码分析
completionService是JUC里的线程池ExecutorCompletionService
3.1 初始化线程池
1、同步初始化一个完成队列completionQueue
3.2 执行submit方法
1、线程池执行的时候传入了自己的内部类QueueingFuture
2、QueueingFuture构造函数,需要关注这里重写了done方法,向阻塞队列里添加task
3、执行FutureTask的run方法
4、set方法
5、在finishCompletion方法中执行了done方法
所以每次线程完成任务,都会往completionQueue中新增一条记录。completionQueue中的记录需要通过别的方式取:ExecutorCompletionService.take()。
ExecutorCompletionService的实际用法应该是这样的:CompletionService一边执行任务,一边处理完成的任务结果,这样可以将执行的任务与处理任务隔离开来进行处理,使用submit执行任务,使用take获取已完成的任务,先完成的任务结果先加入队列。获取的结果,跟提交给线程池的任务是无关联的。
而我们在使用过程中由于需要的是当前线程任务的返回值,所以没有调用take方法,而是通过get方法获取到当前线程调用的返回值,导致task任务在队列中一直增加。
4、问题解决
其实我们想实现原本的需求,可以通过countDownLatch来实现。
5、问题反思
当我们希望引入一些技术来解决现有难题的时候,一定要将该技术的原理进行深入分析,彻底了解后才考虑应用到我们的业务场景中。本次问题虽然是一次内存泄漏,好在实际上没有太大的生产影响。如果我们还是抱着对技术组件原理不了解直接贸然使用的话,往往会导致一些未知的错误,甚至非常严重的后果。