作者:京东零售冯晓涛问题背景 京东生旅平台慧销系统,作为平台系统对接了多条业务线,主要进行各个业务线广告,召回等活动相关内容与能力管理。 最近根据告警发现内存持续升高,每隔23天会收到内存超过阈值告警,猜测可能存在内存泄漏的情况,然后进行排查。根据24小时时间段内存监控可以发现,容器的内存在持续上升: 问题排查 初步估计内存泄漏,查看24小时时间段jvm内存监控,排查jvm内存回收情况: YoungGC和FullGC情况: 通过jvm内存分析和YoungGC与FullGC执行情况,可以判断可能原因如下: 1、存在YoungGC但是没有出现FullGC,可能是对象进入老年代但是没有到达FullGC阈值,所以没有触发FullGC,对象一直存在老年代无法回收 2、存在内存泄漏,虽然执行了YoungGC,但是这部分内存无法被回收 通过线程数监控,观察当前线程情况,发现当前线程数7427个,并且还在不断上升,基本判断存在内存泄漏,并且和线程池的不当使用有关: 通过JStack,获取线程堆栈文件并进行分析,排查为什么会有这么多线程: 发现通过线程池创建的线程数达7000: 代码分析 分析代码中ThreadPoolExecutor的使用场景,发现在一个worker公共类中定义了一个线程池,worker执行时会使用线程池进行异步执行。publicclassBackgroundWorker{privatestaticThreadPoolExecutorthreadPoolExecutor;static{init(15);}publicstaticvoidinit(){init(15);}publicstaticvoidinit(intpoolSize){threadPoolExecutornewThreadPoolExecutor(3,poolSize,1000,TimeUnit。MINUTES,newLinkedBlockingDeque(1000),newThreadPoolExecutor。CallerRunsPolicy());}publicstaticvoidshutdown(){if(threadPoolExecutor!null!threadPoolExecutor。isShutdown()){threadPoolExecutor。shutdownNow();}}publicstaticvoidsubmit(finalRunnabletask){if(tasknull){return;}threadPoolExecutor。execute((){try{task。run();}catch(Exceptione){e。printStackTrace();}});}} 广告缓存刷新worker使用线程池的代码:publicclassAdActivitySyncJob{Scheduled(cron005?)publicvoidexecute(){log。info(AdActivitySyncJobstart);ListDicDTOlocationListlocationService。selectLocation();if(CollectionUtils。isEmpty(locationList)){return;}中间省略部分无关代码BackgroundWorker。init(40);locationCodes。forEach(locationCode{showChannelMap。forEach((key,value){BackgroundWorker。submit(newRunnable(){Overridepublicvoidrun(){log。info(AdActivitySyncJob,locationCode:{},showChannel:{},locationCode,value);ResultresultnotLoginAdActivityOuterService。getAdActivityByLocationInner(locationCode,ImmutableMap。of(showChannel,value));LocalCache。ADACTIVITYCACHE。put(locationCode。concat()。concat(value),result);}});});});log。info(AdActivitySyncJobend);}PostConstructpublicvoidinit(){execute();}} 原因分析:猜测是worker每次执行,都会执行init方法,创建新的线程池,但是局部创建的线程池并没有被关闭,导致内存中的线程池越来越多,ThreadPoolExecutor在使用完成后,如果不手动关闭,无法被GC回收。分析验证 验证局部线程池ThreadPoolExecutor创建后,如果不手动关闭,是否会被GC回收:publicclassTest{privatestaticThreadPoolExecutorthreadPoolExecutor;publicstaticvoidmain(String〔〕args){for(inti1;i100;i){每次均初始化线程池threadPoolExecutornewThreadPoolExecutor(3,15,1000,TimeUnit。MINUTES,newLinkedBlockingDeque(1000),newThreadPoolExecutor。CallerRunsPolicy());使用线程池执行任务for(intj0;j10;j){submit(newRunnable(){Overridepublicvoidrun(){}});}}获取当前所有线程ThreadGroupgroupThread。currentThread()。getThreadGroup();ThreadGrouptopGroupgroup;遍历线程组树,获取根线程组while(group!null){topGroupgroup;groupgroup。getParent();}intslackSizetopGroup。activeCount()2;Thread〔〕slackThreadsnewThread〔slackSize〕;获取根线程组下的所有线程,返回的actualSize便是最终的线程数intactualSizetopGroup。enumerate(slackThreads);Thread〔〕atualThreadsnewThread〔actualSize〕;System。arraycopy(slackThreads,0,atualThreads,0,actualSize);System。out。println(ThreadssizeisatualThreads。length);for(Threadthread:atualThreads){System。out。println(Threadname:thread。getName());}}publicstaticvoidsubmit(finalRunnabletask){if(tasknull){return;}threadPoolExecutor。execute((){try{task。run();}catch(Exceptione){e。printStackTrace();}});}} 输出: Threadssizeis302 Threadname:ReferenceHandler Threadname:Finalizer Threadname:SignalDispatcher Threadname:main Threadname:MonitorCtrlBreak Threadname:pool1thread1 Threadname:pool1thread2 Threadname:pool1thread3 Threadname:pool2thread1 Threadname:pool2thread2 Threadname:pool2thread3 Threadname:pool3thread1 Threadname:pool3thread2 Threadname:pool3thread3 Threadname:pool4thread1 Threadname:pool4thread2 Threadname:pool4thread3 Threadname:pool5thread1 Threadname:pool5thread2 Threadname:pool5thread3 Threadname:pool6thread1 Threadname:pool6thread2 Threadname:pool6thread3 执行结果分析,线程数量302个,局部线程池创建的核心线程没有被回收。 修改初始化线程池部分:初始化一次线程池threadPoolExecutornewThreadPoolExecutor(3,15,1000,TimeUnit。MINUTES,newLinkedBlockingDeque(1000),newThreadPoolExecutor。CallerRunsPolicy());for(inti1;i100;i){使用线程池执行任务for(intj0;j10;j){submit(newRunnable(){Overridepublicvoidrun(){}});}} 输出: Threadssizeis8 Threadname:ReferenceHandler Threadname:Finalizer Threadname:SignalDispatcher Threadname:main Threadname:MonitorCtrlBreak Threadname:pool1thread1 Threadname:pool1thread2 Threadname:pool1thread3解决方案 1、只初始化一次,每次执行worker复用线程池 2、每次执行完成后,关闭线程池 BackgroundWorker的定位是后台执行worker均进行线程池的复用,所以采用方案1,每次在static静态代码块中初始化,使用时无需重新初始化。 解决后监控: jvm内存监控,内存不再持续上升: 线程池恢复正常且平稳: Jstack文件,观察线程池数量恢复正常: Dump文件分析线程池对象数量: 拓展1、如何关闭线程池 线程池提供了两个关闭方法,shutdownNow和shutdown方法。 shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。 shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。2、为什么threadPoolExecutor不会被GC回收threadPoolExecutornewThreadPoolExecutor(3,15,1000,TimeUnit。MINUTES,newLinkedBlockingDeque(1000),newThreadPoolExecutor。CallerRunsPolicy()); 局部使用后未手动关闭的线程池对象,会被GC回收吗?获取线上jump文件进行分析: 发现线程池对象没有被回收,为什么不会被回收?查看ThreadPoolExecutor。execute()方法: 如果当前线程数小于核心线程数,就会进入addWorker方法创建线程: 分析runWorker方法,如果存在任务则执行,否则调用getTask()获取任务: 发现workQueue。take()会一直阻塞,等待队列中的任务,因为Thread线程一直没有结束,存在引用关系:ThreadPoolExecutorWorkerThread,因为存在GCROOT的引用,所以无法被回收。