慧销平台ThreadPoolExecutor内存泄漏分析
作者:京东零售 冯晓涛 问题背景
京东生旅平台慧销系统,作为平台系统对接了多条业务线,主要进行各个业务线广告,召回等活动相关内容与能力管理。
最近根据告警发现内存持续升高,每隔2-3天会收到内存超过阈值告警,猜测可能存在内存泄漏的情况,然后进行排查。根据24小时时间段内存监控可以发现,容器的内存在持续上升:
问题排查
初步估计内存泄漏,查看24小时时间段jvm内存监控,排查jvm内存回收情况:
YoungGC和FullGC情况:
通过jvm内存分析和YoungGC与FullGC执行情况,可以判断可能原因如下:
1、 存在YoungGC但是没有出现FullGC,可能是对象进入老年代但是没有到达FullGC阈值,所以没有触发FullGC,对象一直存在老年代无法回收
2、 存在内存泄漏,虽然执行了YoungGC,但是这部分内存无法被回收
通过线程数监控,观察当前线程情况,发现当前线程数7427个,并且还在不断上升,基本判断存在内存泄漏,并且和线程池的不当使用有关:
通过JStack,获取线程堆栈文件并进行分析,排查为什么会有这么多线程:
发现通过线程池创建的线程数达7000+:
代码分析
分析代码中ThreadPoolExecutor的使用场景,发现在一个worker公共类中定义了一个线程池,worker执行时会使用线程池进行异步执行。 public class BackgroundWorker { private static ThreadPoolExecutor threadPoolExecutor; static { init(15); } public static void init() { init(15); } public static void init(int poolSize) { threadPoolExecutor = new ThreadPoolExecutor(3, poolSize, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); } public static void shutdown() { if (threadPoolExecutor != null && !threadPoolExecutor.isShutdown()) { threadPoolExecutor.shutdownNow(); } } public static void submit(final Runnable task) { if (task == null) { return; } threadPoolExecutor.execute(() -> { try { task.run(); } catch (Exception e) { e.printStackTrace(); } }); } }
广告缓存刷新worker使用线程池的代码: public class AdActivitySyncJob { @Scheduled(cron = "0 0/5 * * * ?") public void execute() { log.info("AdActivitySyncJob start"); List locationList = locationService.selectLocation(); if (CollectionUtils.isEmpty(locationList)) { return; } //中间省略部分无关代码 BackgroundWorker.init(40); locationCodes.forEach(locationCode -> { showChannelMap.forEach((key,value)->{ BackgroundWorker.submit(new Runnable() { @Override public void run() { log.info("AdActivitySyncJob,locationCode:{},showChannel:{}",locationCode,value); Result result = notLoginAdActivityOuterService.getAdActivityByLocationInner(locationCode, ImmutableMap.of("showChannel", value)); LocalCache.AD_ACTIVITY_CACHE.put(locationCode.concat("_").concat(value), result); } }); }); }); log.info("AdActivitySyncJob end"); } @PostConstruct public void init() { execute(); } }
原因分析:猜测是worker每次执行,都会执行init方法,创建新的线程池,但是局部创建的线程池并没有被关闭,导致内存中的线程池越来越多,ThreadPoolExecutor在使用完成后,如果不手动关闭,无法被GC回收。 分析验证
验证局部线程池ThreadPoolExecutor创建后,如果不手动关闭,是否会被GC回收: public class Test { private static ThreadPoolExecutor threadPoolExecutor; public static void main(String[] args) { for (int i=1;i<100;i++){ //每次均初始化线程池 threadPoolExecutor = new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); //使用线程池执行任务 for(int j=0;j<10;j++){ submit(new Runnable() { @Override public void run() { } }); } } //获取当前所有线程 ThreadGroup group = Thread.currentThread().getThreadGroup(); ThreadGroup topGroup = group; // 遍历线程组树,获取根线程组 while (group != null) { topGroup = group; group = group.getParent(); } int slackSize = topGroup.activeCount() * 2; Thread[] slackThreads = new Thread[slackSize]; // 获取根线程组下的所有线程,返回的actualSize便是最终的线程数 int actualSize = topGroup.enumerate(slackThreads); Thread[] atualThreads = new Thread[actualSize]; System.arraycopy(slackThreads, 0, atualThreads, 0, actualSize); System.out.println("Threads size is " + atualThreads.length); for (Thread thread : atualThreads) { System.out.println("Thread name : " + thread.getName()); } } public static void submit(final Runnable task) { if (task == null) { return; } threadPoolExecutor.execute(() -> { try { task.run(); } catch (Exception e) { e.printStackTrace(); } }); } }
输出:
Threads size is 302
Thread name : Reference Handler
Thread name : Finalizer
Thread name : Signal Dispatcher
Thread name : main
Thread name : Monitor Ctrl-Break
Thread name : pool-1-thread-1
Thread name : pool-1-thread-2
Thread name : pool-1-thread-3
Thread name : pool-2-thread-1
Thread name : pool-2-thread-2
Thread name : pool-2-thread-3
Thread name : pool-3-thread-1
Thread name : pool-3-thread-2
Thread name : pool-3-thread-3
Thread name : pool-4-thread-1
Thread name : pool-4-thread-2
Thread name : pool-4-thread-3
Thread name : pool-5-thread-1
Thread name : pool-5-thread-2
Thread name : pool-5-thread-3
Thread name : pool-6-thread-1
Thread name : pool-6-thread-2
Thread name : pool-6-thread-3
…………
执行结果分析,线程数量302个,局部线程池创建的核心线程没有被回收。
修改初始化线程池部分: //初始化一次线程池 threadPoolExecutor = new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); for (int i=1;i<100;i++){ //使用线程池执行任务 for(int j=0;j<10;j++){ submit(new Runnable() { @Override public void run() { } }); } }
输出:
Threads size is 8
Thread name : Reference Handler
Thread name : Finalizer
Thread name : Signal Dispatcher
Thread name : main
Thread name : Monitor Ctrl-Break
Thread name : pool-1-thread-1
Thread name : pool-1-thread-2
Thread name : pool-1-thread-3 解决方案
1、只初始化一次,每次执行worker复用线程池
2、每次执行完成后,关闭线程池
BackgroundWorker的定位是后台执行worker均进行线程池的复用,所以采用方案1,每次在static静态代码块中初始化,使用时无需重新初始化。
解决后监控:
jvm内存监控,内存不再持续上升:
线程池恢复正常且平稳:
Jstack文件,观察线程池数量恢复正常:
Dump文件分析线程池对象数量:
拓展1、 如何关闭线程池
线程池提供了两个关闭方法,shutdownNow 和 shutdown 方法。
shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。
shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。 2、 为什么threadPoolExecutor不会被GC回收threadPoolExecutor = new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());
局部使用后未手动关闭的线程池对象,会被GC回收吗?获取线上jump文件进行分析:
发现线程池对象没有被回收,为什么不会被回收?查看ThreadPoolExecutor.execute()方法:
如果当前线程数小于核心线程数,就会进入addWorker方法创建线程:
分析runWorker方法,如果存在任务则执行,否则调用getTask()获取任务:
发现workQueue.take()会一直阻塞,等待队列中的任务,因为Thread线程一直没有结束, 存在引用关系:ThreadPoolExecutor->Worker->Thread,因为存在GC ROOT的引用,所以无法被回收 。
不要做别人讨厌的人!因为忙着赶路,错过了饭点。正好看见一家饭馆,于是就进到饭馆到里面吃饭。我吃饭有个怪癖,喜欢安静,不喜欢热闹。很排斥那些人挤人人靠人的热闹地方。心想不就是吃个十几块钱的饭,为什么非要
大胆前卫的造型,Ai设计的高定礼服你喜欢吗?Balenciaga篇大胆前卫潮流的造型,Ai设计的高定礼服你喜欢吗?AI大牌设计之Balenciaga篇设计兔最近推出的这个AI大牌高定礼服设计系列,受到很多粉丝朋友的喜欢。AI设计出图的速度和效率极
明星画网红妆长得丑?近期,明星妆容网红化这个话题热度很高。许多人认为,现如今明星的妆容都在向网红靠近,一点也不明星了。同时,也有很多人以此为由去攻击化了网红妆的明星和化妆师们。可是,明星妆容真的在网红
秦岚现身2023春晚联排,穿一身白色长款羽绒服,温柔又保暖!头条创作挑战赛快要临近农历新年了,很多观众都很期待春晚吧?网上也流出很多明星们春晚联排的路透照,这不,秦岚就现身2023春晚联排被拍到了一组照片,咱们可以先来感受一下她的时尚穿搭造
外国人为什么喜欢汉字纹身?相信喜欢看NBA比赛的同学们都会注意到有些球星喜欢纹中国汉字。譬如说这样式的,威尔森钱德勒有的是这样式的,艾弗森还有的是这样式的,肯扬马丁包括一些普通人的纹身,我放几张图片大家自行
嫩模ChantelJeffries的新装已露尖尖角(ChantelJeffries)钱特尔杰弗里斯的新装已露尖尖角周一下午,钱特尔杰弗里斯(ChantelJeffries)在Instagram上分享了几张照片,让粉丝们看到了她的新
爱美女士最简单抗衰美容方法其实,三阴交穴就是我们的父母留给我们的巨额财产。可以帮助我们维持年轻,延缓衰老,推迟更年期,保证女人的魅力。三阴交穴在小腿内侧,脚踩骨的最高点往上三寸处(自己的手横着放,约四根手指
今年流行的运动鞋瑜伽裤,配羽绒服简直天生一对!时髦显瘦自然的美丽,需要我们迎接自己特别的一面。腿粗腿短,又或者有小肚子,这些穿衣的问题都不应该成为你变美的绊脚石。在自我造型提升的路上,每一天都可以尝试全新的设计,不断积累独属于自我的穿
退烧后这样洗头会复阳?能不能洗关键看这1点!冬日生活打卡季要说了阳了的时候最让人难以忍受的一件事就是头发变得又油又臭又痒!出汗让头发变得黏腻,还会散发出酸臭味,一抠指甲里全是白白的油脂颗粒头皮浸渍在汗液里,不仅瘙痒难忍,稍微
张瑜古巷游玩被偶遇!65岁素颜脸美得真实,穿汉服配短发真有韵味总是有人会强调,在什么样的年龄阶段,就应该穿合适的衣服,在年轻的时候,我们可以尽情的尝试各种个性化的服装,是塑造出不同风格的造型,到了中老年阶段,就要打扮的端庄得体一点,才能释放出
请查收!来自他们的致敬有这样一群人守护在城市的每个角落出现在群众需要帮助的每个时刻他们逆光而行寻找真相用行动书写着平安答卷他们有个共同的名字中国人民警察面对畏难,他们决然逆行让党旗和警旗在第一线和最前沿
老头老太的俄罗斯莫斯科与圣彼得堡自由行(五)麻雀山和莫斯科大学,然后是阿尔巴特街以及夜火车7月25号。今天晚上要坐夜火车到圣彼得堡,估计不能够在下午2点返回酒店退房,所以就背着背包游吧。地铁2号线坐3站到剧院站(始发都是从我
西部排名再乱鹈鹕守擂有惊无险,灰熊强势归来,湖人加时赛2分12月10日,全联盟20支球队10场常规赛结束后,西部排名榜再次大乱鹈鹕战胜太阳,守擂成功灰熊掘金强势归来,4支球队并列第7名湖人力拼76人,遗憾的是加时赛仅仅得到2分。1。西部排
漳州云洞岩,最新通告关于入园免查核酸证明的通告尊敬的广大游客们根据我市疫情防控工作的最新要求,即日起,进云洞岩风景区取消查验72小时核酸阴性证明,取消扫码。入园请配合工作人员做好防疫措施,佩戴好口罩,
周末去哪儿去融安,过23甜的冬天这个周末就是金桔的味道!今天(12月9日),第十三届金桔文化旅游节活动在融安县举办,我是融安金桔王桔乡味道美食比赛等活动让人食指大动,恨不得狂炫一大筐金桔!在我们柳州的宝藏县城融安
乘坐绿皮火车感受巴渝文化本周末去铜罐驿冬游吧这个周末去哪儿玩?不如去九龙坡区铜罐驿镇吧!春夏观花,秋冬摘果,乘坐绿皮火车,感受巴渝文化。走进铜罐驿镇,不仅可以体验采摘的乐趣,还能感受到巴渝文化英雄文化驿渡文化多种文化浸润下,
66分冠军加成水花力压双探花勇士再胜绿衫军为西部找回场子勇士赢了,代表整个西部联盟,战胜了此前联盟战绩胜率第一,对阵西部联盟7战全胜的绿衫军,获得了总冠军争夺之后首次交手的胜利。勇士赢了,经历休赛期被拆队赛季初期轮换无人可用,且在目前落
浙江男篮11连胜,许钟豪恶犯遭驱逐,胡明轩状态回暖北京时间12月10日,CBA常规赛第11轮比赛继续进行,今天的比赛CBA安排了两场德比,广东男篮对阵广州男篮,浙江男篮对阵广厦男篮。最终广东男篮和浙江男篮分别赢下了德比之战。浙江男
河北武安翻山越岭村路畅十里八乡豆腐鲜来源人民网河北频道原创稿武安市白云大道。刘涛摄现在我们家的豆腐,可是不愁卖喽。日前,家住河北省武安市活水乡白王庄村的王大爷看着自家刚压制完成的鲜豆腐笑呵呵地说。通过城乡公交邮政物流
208030,湖南公布一批楚怡名单!日前,湖南省教育厅公示了湖南省楚怡文化传承基地楚怡产教融合实训基地楚怡示范性职业教育集团(联盟)计划建设单位名单。名单中涵盖20所湖南省楚怡文化传承基地建设单位80所楚怡产教融合实
必须包含本人,选出历史最佳5人组,库里给出了这个名单最近,库里获得了体育画报评选的年度最佳运动员,随后在接受该平台访谈的时候,主持人让他选出自己心目中的历史最强5人阵容,且必须包含本人。库里稍微思索了一番之后,给出了这样的一份名单我
手机专利大战背后互利互惠,越来越多厂商选择走向和解近日,华为与OPPO签订全球专利交叉许可引发广泛关注同日,苹果与爱立信也结束了旷日持久的专利之战,达成全球专利许可协议。从近期华为OPPO苹果等手机行业巨头的专利诉讼可以看出,更温