作者:京东零售 冯晓涛
问题背景
京东生旅平台慧销系统,作为平台系统对接了多条业务线,主要进行各个业务线广告,召回等活动相关内容与能力管理。
最近根据告警发现内存持续升高,每隔2-3天会收到内存超过阈值告警,猜测可能存在内存泄漏的情况,然后进行排查。根据24小时时间段内存监控可以发现,容器的内存在持续上升:
问题排查
初步估计内存泄漏,查看24小时时间段jvm内存监控,排查jvm内存回收情况:
YoungGC和FullGC情况:
通过jvm内存分析和YoungGC与FullGC执行情况,可以判断可能原因如下:
1、 存在YoungGC但是没有出现FullGC,可能是对象进入老年代但是没有到达FullGC阈值,所以没有触发FullGC,对象一直存在老年代无法回收
2、 存在内存泄漏,虽然执行了YoungGC,但是这部分内存无法被回收
通过线程数监控,观察当前线程情况,发现当前线程数7427个,并且还在不断上升,基本判断存在内存泄漏,并且和线程池的不当使用有关:
通过JStack,获取线程堆栈文件并进行分析,排查为什么会有这么多线程:
发现通过线程池创建的线程数达7000+:
代码分析
分析代码中ThreadPoolExecutor的使用场景,发现在一个worker公共类中定义了一个线程池,worker执行时会使用线程池进行异步执行。
1 public class BackgroundWorker { 2 3 private static ThreadPoolExecutor threadPoolExecutor; 4 5 static { 6 init(15); 7 } 8 9 public static void init() { 10 init(15); 11 } 12 13 public static void init(int poolSize) { 14 threadPoolExecutor = 15 new ThreadPoolExecutor(3, poolSize, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); 16 } 17 18 public static void shutdown() { 19 if (threadPoolExecutor != null && !threadPoolExecutor.isShutdown()) { 20 threadPoolExecutor.shutdownNow(); 21 } 22 } 23 24 public static void submit(final Runnable task) { 25 if (task == null) { 26 return; 27 } 28 threadPoolExecutor.execute(() -> { 29 try { 30 task.run(); 31 } catch (Exception e) { 32 e.printStackTrace(); 33 } 34 }); 35 } 36 37 } 38
广告缓存刷新worker使用线程池的代码:
1 public class AdActivitySyncJob { 2 3 @Scheduled(cron = "0 0/5 * * * ?") 4 public void execute() { 5 log.info("AdActivitySyncJob start"); 6 List<DicDTO> locationList = locationService.selectLocation(); 7 if (CollectionUtils.isEmpty(locationList)) { 8 return; 9 } 10 11 //中间省略部分无关代码 12 13 BackgroundWorker.init(40); 14 locationCodes.forEach(locationCode -> { 15 showChannelMap.forEach((key,value)->{ 16 BackgroundWorker.submit(new Runnable() { 17 @Override 18 public void run() { 19 log.info("AdActivitySyncJob,locationCode:{},showChannel:{}",locationCode,value); 20 Result<AdActivityDTO> result = notLoginAdActivityOuterService.getAdActivityByLocationInner(locationCode, ImmutableMap.of("showChannel", value)); 21 LocalCache.AD_ACTIVITY_CACHE.put(locationCode.concat("_").concat(value), result); 22 } 23 }); 24 }); 25 }); 26 log.info("AdActivitySyncJob end"); 27 } 28 29 @PostConstruct 30 public void init() { 31 execute(); 32 } 33} 34
原因分析:猜测是worker每次执行,都会执行init方法,创建新的线程池,但是局部创建的线程池并没有被关闭,导致内存中的线程池越来越多,ThreadPoolExecutor在使用完成后,如果不手动关闭,无法被GC回收。
分析验证
验证局部线程池ThreadPoolExecutor创建后,如果不手动关闭,是否会被GC回收:
1public class Test { 2 private static ThreadPoolExecutor threadPoolExecutor; 3 4 public static void main(String[] args) { 5 for (int i=1;i<100;i++){ 6 //每次均初始化线程池 7 threadPoolExecutor = 8 new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); 9 10 //使用线程池执行任务 11 for(int j=0;j<10;j++){ 12 submit(new Runnable() { 13 @Override 14 public void run() { 15 } 16 }); 17 } 18 19 } 20 //获取当前所有线程 21 ThreadGroup group = Thread.currentThread().getThreadGroup(); 22 ThreadGroup topGroup = group; 23 // 遍历线程组树,获取根线程组 24 while (group != null) { 25 topGroup = group; 26 group = group.getParent(); 27 } 28 int slackSize = topGroup.activeCount() * 2; 29 Thread[] slackThreads = new Thread[slackSize]; 30 // 获取根线程组下的所有线程,返回的actualSize便是最终的线程数 31 int actualSize = topGroup.enumerate(slackThreads); 32 Thread[] atualThreads = new Thread[actualSize]; 33 System.arraycopy(slackThreads, 0, atualThreads, 0, actualSize); 34 System.out.println("Threads size is " + atualThreads.length); 35 for (Thread thread : atualThreads) { 36 System.out.println("Thread name : " + thread.getName()); 37 } 38 } 39 40 public static void submit(final Runnable task) { 41 if (task == null) { 42 return; 43 } 44 threadPoolExecutor.execute(() -> { 45 try { 46 task.run(); 47 } catch (Exception e) { 48 e.printStackTrace(); 49 } 50 }); 51 } 52} 53
输出:
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个,局部线程池创建的核心线程没有被回收。
修改初始化线程池部分:
1//初始化一次线程池 2threadPoolExecutor = 3 new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); 4 5for (int i=1;i<100;i++){ 6 //使用线程池执行任务 7 for(int j=0;j<10;j++){ 8 submit(new Runnable() { 9 @Override 10 public void run() { 11 } 12 }); 13 } 14 15} 16
输出:
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回收
1threadPoolExecutor = 2 new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy()); 3
局部使用后未手动关闭的线程池对象,会被GC回收吗?获取线上jump文件进行分析:
发现线程池对象没有被回收,为什么不会被回收?查看ThreadPoolExecutor.execute()方法:
如果当前线程数小于核心线程数,就会进入addWorker方法创建线程:
分析runWorker方法,如果存在任务则执行,否则调用getTask()获取任务:
发现workQueue.take()会一直阻塞,等待队列中的任务,因为Thread线程一直没有结束, 存在引用关系:ThreadPoolExecutor->Worker->Thread,因为存在GC ROOT的引用,所以无法被回收 。
