经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » 编程经验 » 查看文章
慧销平台ThreadPoolExecutor内存泄漏分析
来源:cnblogs  作者:京东云开发者  时间:2023/2/28 8:51:22  对本文有异议

作者:京东零售 冯晓涛

问题背景

京东生旅平台慧销系统,作为平台系统对接了多条业务线,主要进行各个业务线广告,召回等活动相关内容与能力管理。

最近根据告警发现内存持续升高,每隔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. private static ThreadPoolExecutor threadPoolExecutor;
  3. static {
  4. init(15);
  5. }
  6. public static void init() {
  7. init(15);
  8. }
  9. public static void init(int poolSize) {
  10. threadPoolExecutor =
  11. new ThreadPoolExecutor(3, poolSize, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());
  12. }
  13. public static void shutdown() {
  14. if (threadPoolExecutor != null && !threadPoolExecutor.isShutdown()) {
  15. threadPoolExecutor.shutdownNow();
  16. }
  17. }
  18. public static void submit(final Runnable task) {
  19. if (task == null) {
  20. return;
  21. }
  22. threadPoolExecutor.execute(() -> {
  23. try {
  24. task.run();
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. }
  28. });
  29. }
  30. }

广告缓存刷新worker使用线程池的代码:

  1.  public class AdActivitySyncJob {
  2.     @Scheduled(cron = "0 0/5 * * * ?")
  3.     public void execute() {
  4.         log.info("AdActivitySyncJob start");
  5.         List<DicDTO> locationList = locationService.selectLocation();
  6.         if (CollectionUtils.isEmpty(locationList)) {
  7.             return;
  8.         }
  9.         //中间省略部分无关代码
  10. BackgroundWorker.init(40);
  11.         locationCodes.forEach(locationCode -> {
  12.             showChannelMap.forEach((key,value)->{
  13.                 BackgroundWorker.submit(new Runnable() {
  14.                     @Override
  15.                     public void run() {
  16.                         log.info("AdActivitySyncJob,locationCode:{},showChannel:{}",locationCode,value);
  17.                         Result<AdActivityDTO> result = notLoginAdActivityOuterService.getAdActivityByLocationInner(locationCode, ImmutableMap.of("showChannel", value));
  18.                         LocalCache.AD_ACTIVITY_CACHE.put(locationCode.concat("_").concat(value), result);
  19.                     }
  20.                 });
  21.             });
  22.         });
  23.         log.info("AdActivitySyncJob end");
  24.     }
  25.     @PostConstruct
  26.     public void init() {
  27.         execute();
  28.     }
  29. }

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

分析验证

验证局部线程池ThreadPoolExecutor创建后,如果不手动关闭,是否会被GC回收:

  1. public class Test {
  2.     private static ThreadPoolExecutor threadPoolExecutor;
  3.     public static void main(String[] args) {
  4.         for (int i=1;i<100;i++){
  5.             //每次均初始化线程池
  6.             threadPoolExecutor =
  7.                     new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());
  8.             //使用线程池执行任务
  9.             for(int j=0;j<10;j++){
  10.                 submit(new Runnable() {
  11.                     @Override
  12.                     public void run() {
  13.                     }
  14.                 });
  15.             }
  16.         }
  17.         //获取当前所有线程
  18.         ThreadGroup group = Thread.currentThread().getThreadGroup();
  19.         ThreadGroup topGroup = group;
  20.         // 遍历线程组树,获取根线程组
  21.         while (group != null) {
  22.             topGroup = group;
  23.             group = group.getParent();
  24.         }
  25.         int slackSize = topGroup.activeCount() * 2;
  26.         Thread[] slackThreads = new Thread[slackSize];
  27.         // 获取根线程组下的所有线程,返回的actualSize便是最终的线程数
  28.         int actualSize = topGroup.enumerate(slackThreads);
  29.         Thread[] atualThreads = new Thread[actualSize];
  30.         System.arraycopy(slackThreads, 0, atualThreads, 0, actualSize);
  31.         System.out.println("Threads size is " + atualThreads.length);
  32.         for (Thread thread : atualThreads) {
  33.             System.out.println("Thread name : " + thread.getName());
  34.         }
  35.     }
  36.     public static void submit(final Runnable task) {
  37.         if (task == null) {
  38.             return;
  39.         }
  40.         threadPoolExecutor.execute(() -> {
  41.             try {
  42.                 task.run();
  43.             } catch (Exception e) {
  44.                 e.printStackTrace();
  45.             }
  46.         });
  47.     }
  48. }

输出:

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. //初始化一次线程池
  2. threadPoolExecutor =
  3.         new ThreadPoolExecutor(3, 15, 1000, TimeUnit.MINUTES, new LinkedBlockingDeque<>(1000), new ThreadPoolExecutor.CallerRunsPolicy());
  4. for (int i=1;i<100;i++){
  5.     //使用线程池执行任务
  6.     for(int j=0;j<10;j++){
  7.         submit(new Runnable() {
  8.             @Override
  9.             public void run() {
  10.             }
  11.         });
  12.     }
  13. }

输出:

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回收

  1. threadPoolExecutor =
  2. 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的引用,所以无法被回收 。

原文链接:https://www.cnblogs.com/Jcloud/p/17162600.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号