JAVA知识体系之问题处理篇(三)——用错CompletionService导致的内存泄漏问题

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、问题反思

  当我们希望引入一些技术来解决现有难题的时候,一定要将该技术的原理进行深入分析,彻底了解后才考虑应用到我们的业务场景中。本次问题虽然是一次内存泄漏,好在实际上没有太大的生产影响。如果我们还是抱着对技术组件原理不了解直接贸然使用的话,往往会导致一些未知的错误,甚至非常严重的后果。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值