作者:京东零售 冯晓涛

问题背景

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

最近根据告警发现内存持续升高,每隔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的引用,所以无法被回收 。

慧销平台ThreadPoolExecutor内存泄漏分析的更多相关文章

  1. Java内存泄漏分析系列之五:常见的Thread Dump日志案例分析

    原文地址:http://www.javatang.com 症状及解决方案 下面列出几种常见的症状即对应的解决方案: CPU占用率很高,响应很慢 按照<Java内存泄漏分析系列之一:使用jstac ...

  2. Android内存泄漏分析实战

    内存泄漏简单介绍 java能够保证当没有引用指向对象的时候,对象会被垃圾回收器回收.与c语言自己申请的内存自己释放相比,java程序猿轻松了非常多.可是并不代表java程序猿不用操心内存泄漏.当jav ...

  3. 内存泄漏分析工具tMemMonitor (TMM)使用简介

    C/C++由于灵活.高效的优点一直以来都是主流的程序设计语言之一,但是其内存的分配与释放均由程序员自己管理,当由于疏忽或错误造成程序未能释放不再使用的内存时就会造成内存泄漏.在大型.复杂的应用程序中, ...

  4. Java内存泄漏分析与解决方案

    Java内存泄漏是每个Java程序员都会遇到的问题,程序在本地运行一切正常,可是布署到远端就会出现内存无限制的增长,最后系统瘫痪,那么如何最快最好的检测程序的稳定性,防止系统崩盘,作者用自已的亲身经历 ...

  5. Android内存泄漏分析及调试

    尊重原创作者,转载请注明出处: http://blog.csdn.net/gemmem/article/details/13017999 此文承接我的另一篇文章:Android进程的内存管理分析 首先 ...

  6. Android 内存泄漏分析与解决方法

    在分析Android内存泄漏之前,先了解一下JAVA的一些知识 1. JAVA中的对象的创建 使用new指令生成对象时,堆内存将会为此开辟一份空间存放该对象 垃圾回收器回收非存活的对象,并释放对应的内 ...

  7. Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析

    原文地址:http://www.javatang.com 一个典型的thread dump文件主要由一下几个部分组成: 上图将JVM上的线程堆栈信息和线程信息做了详细的拆解. 第一部分:Full th ...

  8. Javascript的内存泄漏分析

    作为程序员(更高大尚的称谓:研软件研发)的我们,无论是用Javascript,还是.net, java语言,肯定都遇到过内存泄漏的问题.只不过他们都有GC机制来帮助程序员完成内存回收的事情,如果你是C ...

  9. (转)Android内存泄漏分析及调试

      http://blog.csdn.net/gemmem/article/details/13017999 此文承接我的另一篇文章:Android进程的内存管理分析  首先了解一下dalvik的Ga ...

  10. 使用Eclipse Memory Analyzer进行内存泄漏分析三部曲

    源地址:http://seanhe.iteye.com/blog/898277 一.准备工作  分析较大的dump文件(根据我自己的经验2G以上的dump文件就需要使用以下介绍的方法,不然mat会出现 ...

随机推荐

  1. Springboot优雅进行字段检验

    Springboot优雅进行字段检验 1.Controller VS Service 推荐与业务无关的放在controller层中进行校验,而与业务相关的放在service层中校验. 2.常用校验工具 ...

  2. kali使用命令ifconfig查询ip地址一直为127.0.0.1的解决办法

    解决方法: 执行命令:dhclient -v,即可解决

  3. JavaScript:操作符:逻辑运算符及其隐式转换数据类型

    逻辑非! 用来对布尔值进行取反,即!true = false: 当取反的变量不是布尔值,会进行隐式转换为布尔值: 非0的数字,都转换为true 非空字符串,转换为true 非空对象,转换为true I ...

  4. 如何配置 SLO

    前言 无论是对外提供 IaaS PaaS SaaS 的云公司,还是提供信息技术服务的乙方公司,亦或是金融 制造等各行各业的数据中心.运维部门,我们的一个非常重要的合同承诺或考核评估指标就是:SLA(即 ...

  5. Redis-03 Redis事务

    需要特别注意,Redis 的命令是原子性的,而 Redis 的事务是非原子性的 事务相关命令 MULTI 命令 开启事务命令,Redis将操作命令逐个放到队列中,根据EXEC命令来原子化执行命令 EX ...

  6. 《机器人SLAM导航核心技术与实战》第1季:第4章_机器人传感器

    <机器人SLAM导航核心技术与实战>第1季:第4章_机器人传感器 视频讲解 [第1季]4.第4章_机器人传感器-视频讲解 [第1季]4.1.第4章_机器人传感器_惯性测量单元-视频讲解 [ ...

  7. 【学习笔记】XR872 GUI Littlevgl 8.0 移植(显示部分)

    LVGL 介绍 官方网站:LVGL - Light and Versatile Embedded Graphics Library 源码位置:GitHub - lvgl/lvgl: Powerful ...

  8. Springboot整合AOP和注解,实现丰富的切面功能

    简介 我们在文章<Spring AOP与AspectJ的对比及应用>介绍了AOP的使用,这篇文章讲解一下AOP与注解的整合,通过注解来使用AOP,会非常方便.为了简便,我们还是来实现一个计 ...

  9. ChatGPT集成之前,让我们复习一下即将过时的知识

    各大搜索引擎集成 ChatGPT 的步调已经在逐步加紧了.也许这将极大的改变搜索引擎的生态.那么就让我们在时代迎来巨变之前,复习一下即将过时的搜索引擎知识吧. 搜索引擎一般查询规则 在搜索引擎的时代, ...

  10. 带你动手做AI版的垃圾分类

    摘要:本案例将使用YOLOX模型,实现一个简单的垃圾分类应用. 本文分享自华为云社区<ModelBox社区案例 - 使用YOLOX做垃圾分类>,作者:HWCloudAI. 1 ModelB ...