慧销平台ThreadPoolExecutor内存泄漏分析

作者:京东零售 冯晓涛4Hk致力于为用户收集丰富的生活经验知识

问题背景

京东生旅平台慧销系统,作为平台系统对接了多条业务线,主要进行各个业务线广告,召回等活动相关内容与能力管理。4Hk致力于为用户收集丰富的生活经验知识

最近根据告警发现内存持续升高,每隔2-3天会收到内存超过阈值告警,猜测可能存在内存泄漏的情况,然后进行排查。根据24小时时间段内存监控可以发现,容器的内存在持续上升:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

问题排查

初步估计内存泄漏,查看24小时时间段jvm内存监控,排查jvm内存回收情况:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

YoungGC和FullGC情况:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

通过jvm内存分析和YoungGC与FullGC执行情况,可以判断可能原因如下:4Hk致力于为用户收集丰富的生活经验知识

1、 存在YoungGC但是没有出现FullGC,可能是对象进入老年代但是没有到达FullGC阈值,所以没有触发FullGC,对象一直存在老年代无法回收4Hk致力于为用户收集丰富的生活经验知识

2、 存在内存泄漏,虽然执行了YoungGC,但是这部分内存无法被回收4Hk致力于为用户收集丰富的生活经验知识

通过线程数监控,观察当前线程情况,发现当前线程数7427个,并且还在不断上升,基本判断存在内存泄漏,并且和线程池的不当使用有关:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

通过JStack,获取线程堆栈文件并进行分析,排查为什么会有这么多线程:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

发现通过线程池创建的线程数达7000+:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

代码分析

分析代码中ThreadPoolExecutor的使用场景,发现在一个worker公共类中定义了一个线程池,worker执行时会使用线程池进行异步执行。4Hk致力于为用户收集丰富的生活经验知识

public class BackgroundWorker {4Hk致力于为用户收集丰富的生活经验知识

private static ThreadPoolExecutor threadPoolExecutor;4Hk致力于为用户收集丰富的生活经验知识

static {4Hk致力于为用户收集丰富的生活经验知识

init(15);4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

public static void init() {4Hk致力于为用户收集丰富的生活经验知识

init(15);4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

public static void init(int poolSize) {4Hk致力于为用户收集丰富的生活经验知识

threadPoolExecutor =4Hk致力于为用户收集丰富的生活经验知识

new ThreadPoolExecutor(3, poolSize, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

public static void shutdown() {4Hk致力于为用户收集丰富的生活经验知识

if (threadPoolExecutor != null && !threadPoolExecutor.isShutdown()) {4Hk致力于为用户收集丰富的生活经验知识

threadPoolExecutor.shutdownNow();4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

public static void submit(final Runnable task) {4Hk致力于为用户收集丰富的生活经验知识

if (task == null) {4Hk致力于为用户收集丰富的生活经验知识

return;4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

threadPoolExecutor.execute(() -> {4Hk致力于为用户收集丰富的生活经验知识

try {4Hk致力于为用户收集丰富的生活经验知识

task.run();4Hk致力于为用户收集丰富的生活经验知识

} catch (Exception e) {4Hk致力于为用户收集丰富的生活经验知识

e.printStackTrace();4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

});4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

广告缓存刷新worker使用线程池的代码:4Hk致力于为用户收集丰富的生活经验知识

public class AdActivitySyncJob {4Hk致力于为用户收集丰富的生活经验知识

@Scheduled(cron = "0 0/5 * * * ?")4Hk致力于为用户收集丰富的生活经验知识

public void execute() {4Hk致力于为用户收集丰富的生活经验知识

log.info("AdActivitySyncJob start");4Hk致力于为用户收集丰富的生活经验知识

List<DicDTO> locationList = locationService.selectLocation();4Hk致力于为用户收集丰富的生活经验知识

if (CollectionUtils.isEmpty(locationList)) {4Hk致力于为用户收集丰富的生活经验知识

return;4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

//中间省略部分无关代码4Hk致力于为用户收集丰富的生活经验知识

BackgroundWorker.init(40);4Hk致力于为用户收集丰富的生活经验知识

locationCodes.forEach(locationCode -> {4Hk致力于为用户收集丰富的生活经验知识

showChannelMap.forEach((key,value)->{4Hk致力于为用户收集丰富的生活经验知识

BackgroundWorker.submit(new Runnable() {4Hk致力于为用户收集丰富的生活经验知识

@Override4Hk致力于为用户收集丰富的生活经验知识

public void run() {4Hk致力于为用户收集丰富的生活经验知识

log.info("AdActivitySyncJob,locationCode:{},showChannel:{}",locationCode,value);4Hk致力于为用户收集丰富的生活经验知识

Result<AdActivityDTO> result = notLoginAdActivityOuterService.getAdActivityByLocationInner(locationCode, ImmutableMap.of("showChannel", value));4Hk致力于为用户收集丰富的生活经验知识

LocalCache.AD_ACTIVITY_CACHE.put(locationCode.concat("_").concat(value), result);4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

});4Hk致力于为用户收集丰富的生活经验知识

});4Hk致力于为用户收集丰富的生活经验知识

});4Hk致力于为用户收集丰富的生活经验知识

log.info("AdActivitySyncJob end");4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

@PostConstruct4Hk致力于为用户收集丰富的生活经验知识

public void init() {4Hk致力于为用户收集丰富的生活经验知识

execute();4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

原因分析:猜测是worker每次执行,都会执行init方法,创建新的线程池,但是局部创建的线程池并没有被关闭,导致内存中的线程池越来越多,ThreadPoolExecutor在使用完成后,如果不手动关闭,无法被GC回收。4Hk致力于为用户收集丰富的生活经验知识

分析验证

验证局部线程池ThreadPoolExecutor创建后,如果不手动关闭,是否会被GC回收:4Hk致力于为用户收集丰富的生活经验知识

public class Test {4Hk致力于为用户收集丰富的生活经验知识

private static ThreadPoolExecutor threadPoolExecutor;4Hk致力于为用户收集丰富的生活经验知识

public static void main(String[] args) {4Hk致力于为用户收集丰富的生活经验知识

for (int i=1;i<100;i++){4Hk致力于为用户收集丰富的生活经验知识

//每次均初始化线程池4Hk致力于为用户收集丰富的生活经验知识

threadPoolExecutor =4Hk致力于为用户收集丰富的生活经验知识

new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());4Hk致力于为用户收集丰富的生活经验知识

//使用线程池执行任务4Hk致力于为用户收集丰富的生活经验知识

for(int j=0;j<10;j++){4Hk致力于为用户收集丰富的生活经验知识

submit(new Runnable() {4Hk致力于为用户收集丰富的生活经验知识

@Override4Hk致力于为用户收集丰富的生活经验知识

public void run() {4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

});4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

//获取当前所有线程4Hk致力于为用户收集丰富的生活经验知识

ThreadGroup group = Thread.currentThread().getThreadGroup();4Hk致力于为用户收集丰富的生活经验知识

ThreadGroup topGroup = group;4Hk致力于为用户收集丰富的生活经验知识

// 遍历线程组树,获取根线程组4Hk致力于为用户收集丰富的生活经验知识

while (group != null) {4Hk致力于为用户收集丰富的生活经验知识

topGroup = group;4Hk致力于为用户收集丰富的生活经验知识

group = group.getParent();4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

int slackSize = topGroup.activeCount() * 2;4Hk致力于为用户收集丰富的生活经验知识

Thread[] slackThreads = new Thread[slackSize];4Hk致力于为用户收集丰富的生活经验知识

// 获取根线程组下的所有线程,返回的actualSize便是最终的线程数4Hk致力于为用户收集丰富的生活经验知识

int actualSize = topGroup.enumerate(slackThreads);4Hk致力于为用户收集丰富的生活经验知识

Thread[] atualThreads = new Thread[actualSize];4Hk致力于为用户收集丰富的生活经验知识

System.arraycopy(slackThreads, 0, atualThreads, 0, actualSize);4Hk致力于为用户收集丰富的生活经验知识

System.out.println("Threads size is " + atualThreads.length);4Hk致力于为用户收集丰富的生活经验知识

for (Thread thread : atualThreads) {4Hk致力于为用户收集丰富的生活经验知识

System.out.println("Thread name : " + thread.getName());4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

public static void submit(final Runnable task) {4Hk致力于为用户收集丰富的生活经验知识

if (task == null) {4Hk致力于为用户收集丰富的生活经验知识

return;4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

threadPoolExecutor.execute(() -> {4Hk致力于为用户收集丰富的生活经验知识

try {4Hk致力于为用户收集丰富的生活经验知识

task.run();4Hk致力于为用户收集丰富的生活经验知识

} catch (Exception e) {4Hk致力于为用户收集丰富的生活经验知识

e.printStackTrace();4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

});4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

输出:4Hk致力于为用户收集丰富的生活经验知识

Threads size is 3024Hk致力于为用户收集丰富的生活经验知识

Thread name : Reference Handler4Hk致力于为用户收集丰富的生活经验知识

Thread name : Finalizer4Hk致力于为用户收集丰富的生活经验知识

Thread name : Signal Dispatcher4Hk致力于为用户收集丰富的生活经验知识

Thread name : main4Hk致力于为用户收集丰富的生活经验知识

Thread name : Monitor Ctrl-Break4Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-1-thread-14Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-1-thread-24Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-1-thread-34Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-2-thread-14Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-2-thread-24Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-2-thread-34Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-3-thread-14Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-3-thread-24Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-3-thread-34Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-4-thread-14Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-4-thread-24Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-4-thread-34Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-5-thread-14Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-5-thread-24Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-5-thread-34Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-6-thread-14Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-6-thread-24Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-6-thread-34Hk致力于为用户收集丰富的生活经验知识

…………4Hk致力于为用户收集丰富的生活经验知识

执行结果分析,线程数量302个,局部线程池创建的核心线程没有被回收。4Hk致力于为用户收集丰富的生活经验知识

修改初始化线程池部分:4Hk致力于为用户收集丰富的生活经验知识

//初始化一次线程池4Hk致力于为用户收集丰富的生活经验知识

threadPoolExecutor =4Hk致力于为用户收集丰富的生活经验知识

new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());4Hk致力于为用户收集丰富的生活经验知识

for (int i=1;i<100;i++){4Hk致力于为用户收集丰富的生活经验知识

//使用线程池执行任务4Hk致力于为用户收集丰富的生活经验知识

for(int j=0;j<10;j++){4Hk致力于为用户收集丰富的生活经验知识

submit(new Runnable() {4Hk致力于为用户收集丰富的生活经验知识

@Override4Hk致力于为用户收集丰富的生活经验知识

public void run() {4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

});4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

}4Hk致力于为用户收集丰富的生活经验知识

输出:4Hk致力于为用户收集丰富的生活经验知识

Threads size is 84Hk致力于为用户收集丰富的生活经验知识

Thread name : Reference Handler4Hk致力于为用户收集丰富的生活经验知识

Thread name : Finalizer4Hk致力于为用户收集丰富的生活经验知识

Thread name : Signal Dispatcher4Hk致力于为用户收集丰富的生活经验知识

Thread name : main4Hk致力于为用户收集丰富的生活经验知识

Thread name : Monitor Ctrl-Break4Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-1-thread-14Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-1-thread-24Hk致力于为用户收集丰富的生活经验知识

Thread name : pool-1-thread-34Hk致力于为用户收集丰富的生活经验知识

解决方案

1、只初始化一次,每次执行worker复用线程池4Hk致力于为用户收集丰富的生活经验知识

2、每次执行完成后,关闭线程池4Hk致力于为用户收集丰富的生活经验知识

BackgroundWorker的定位是后台执行worker均进行线程池的复用,所以采用方案1,每次在static静态代码块中初始化,使用时无需重新初始化。4Hk致力于为用户收集丰富的生活经验知识

解决后监控:4Hk致力于为用户收集丰富的生活经验知识

jvm内存监控,内存不再持续上升:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

线程池恢复正常且平稳:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

Jstack文件,观察线程池数量恢复正常:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

Dump文件分析线程池对象数量:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

拓展

1、 如何关闭线程池4Hk致力于为用户收集丰富的生活经验知识

线程池提供了两个关闭方法,shutdownNow 和 shutdown 方法。4Hk致力于为用户收集丰富的生活经验知识

shutdownNow方法的解释是:线程池拒接收新提交的任务,同时立马关闭线程池,线程池里的任务不再执行。4Hk致力于为用户收集丰富的生活经验知识

shutdown方法的解释是:线程池拒接收新提交的任务,同时等待线程池里的任务执行完毕后关闭线程池。4Hk致力于为用户收集丰富的生活经验知识

2、 为什么threadPoolExecutor不会被GC回收4Hk致力于为用户收集丰富的生活经验知识

threadPoolExecutor =4Hk致力于为用户收集丰富的生活经验知识

new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());4Hk致力于为用户收集丰富的生活经验知识

局部使用后未手动关闭的线程池对象,会被GC回收吗?获取线上jump文件进行分析:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

发现线程池对象没有被回收,为什么不会被回收?查看ThreadPoolExecutor.execute()方法:4Hk致力于为用户收集丰富的生活经验知识

如果当前线程数小于核心线程数,就会进入addWorker方法创建线程:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

分析runWorker方法,如果存在任务则执行,否则调用getTask()获取任务:4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

4Hk致力于为用户收集丰富的生活经验知识

发现workQueue.take()会一直阻塞,等待队列中的任务,因为Thread线程一直没有结束, 存在引用关系:ThreadPoolExecutor->Worker->Thread,因为存在GC ROOT的引用,所以无法被回收 。4Hk致力于为用户收集丰富的生活经验知识

也许你还喜欢

鹅鸭杀由于网络问题无法进入房间

鹅鸭杀不显示游戏房间、无法加入房间是游戏网络不适合本地网络的原因,鹅鸭杀作为一款海

传统制造企业有必要建设增材制造中

在我们日常娱乐和日常工作中,我们如果想将FLV格式视频转换为MP4文件该怎么办呢?今天就

验证码无法显示怎么办图文介绍

验证码图片有些时候不能显示,那我们怎么办呢?下面将为大家讲解关于验证码无法显示的解决

公司电子印章生产制作图文教程

印章,用作印于文件上表示鉴定或签署的文具,一般印章都会先沾上颜料再印上,不沾颜料、印上

手机信息加密软件有哪些

今天给大家推荐加密软件排行榜,当然了,根据不同的排行和标准,加密软件排行榜前五名可能有

打字机效果怎么做图文介绍

如何在PPT中制作打字机效果呢?下面就是具体的实现方法。

百度文档下载器怎么用图文教程

很多小伙伴都知道百度文库中的很多资料资源下载下来都是需要付费或者是开通会员才能够

怎么去除视频水印方法图文详解

怎么用视频水印去除工具给视频去水印呢?我们可以用AE软件来去除,现在我来教大家吧!

android退出程序的几种方法

清除不使用的应用程序超出了应用程序管理的范围。它可以优化设备的性能并延长电池寿命

移动端关键词优化软件有哪些

SEO关键词排名软件,正是帮助网站在搜索引擎结果中获得更好排名的利器。