慧销平台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<DicDTO> 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<AdActivityDTO> 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的引用,所以无法被回收 。

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Linux 引入新的 SSDFS 文件系统,针对 ZNS SSD 进行优化
Linux 6.3 最新补丁引入了 SSDFS 文件系统支持,这个新的开源文件系统特别针对 ZNS SSD (支持 NVMe 分区命名空间 (ZNS) 的固态硬盘)进行了优化。 ZNS (Zoned Namespaces)SSD 即分区命名空间固态硬盘,通过 NVMe 的新规范实现。ZNS SSD 将容量划分为 zone(分区),在namespace(命名空间)里线性分布。每个zone可以按任意顺序读取,但必须按顺序写入,且在再一次写入前必须要先重置。此外,其 ZNS 命令集公开主机系统和 SSD 之间的分区块存储接口,允许更具优化的数据对齐。 这些特性允许 ZNS SSD 改善其内部数据放置,通过降低 I/O 访问延迟、提高写吞吐量、更低的 QoS 和更大的容量来获得更高的性能。 尽管内核主线已存在 F2FS “闪存友好文件系统”,但从 SSDFS 的介绍来看,该文件系统特对支持 NVMe ZNS 的固态硬盘有特别的优化,比如通过消除垃圾收集的“GC”开销,延长 SSD 寿命/耐用性,且原生支持 ZNS SSD 和 SMR HDD 的严格“仅附加模式”,保证强大的可靠性和稳定的性能。...
- 下一篇
巧妙利用“慧言”机器人在安全场景中实践
作者:CCO体系 郝帅卫 一、前言 提到机器人在日常工作中充满着让人便利的想法,对于机器人的交互其中比较有名的为“微信公众号/服务号”,可以通过便捷聊天的窗口形式,输入特定的指令可完成对应的动作;这里我们举一个例子:我想输入“查询今日天气”,那么可利用聊天框,发送聊天/语音的形式写到输入框“点击发送”,服务端在收集到查询今日天气指令之后再结合用户的特征信息,完成天气检索/组装,自动的形式以聊天框回复。这样的一个场景就可以节省我们去打其它APP或者网站去查询,直接全部闭环在“微信中”。 公众号虽然强大,但是对于集团内部特别是涉及信息安全就需要利用内部的软件完成以上形式的操作。在之前有对京me的机器人和应用了解,应用需要独立开发/发布/上线一系列环节比较重,后又了解到机器人,但是之前的机器人只能做通过配置好的关键字完成自动回复,无法通过自定义指令返回想要的信息,只能特定一问一答;比如:“我要吃饭”,命中之后,会固定的返回:“大米、馒头、咸菜”.....。再之后京me推出“多轮对话”能力,该能力支持了通过指令的方式,完成与服务端交互,达到灵活定制。这时候再输入“我要吃饭”,那服务端就可以根据...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS6,CentOS7官方镜像安装Oracle11G
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2更换Tomcat为Jetty,小型站点的福音
- Red5直播服务器,属于Java语言的直播服务器
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作