一次线程池未关闭导致的内存泄露问题

背景,运营反馈接口查询越用越卡。

排查方案,跳板机登录到服务器,top命令查看,内存占用过高,

使用jmap分析:

jmap -histo:live 858524

Thread类型的对象占用的内存最多。

使用jstack命令分析:

jstack -l 858524

可以看到线程池中有很多线程处于WAITING状态,遗憾的是没有对线程池进行命名,不确定是在哪创建的线程池。

在MAT中分析(对本次没实际意义,只单纯使用)

使用jmap -dump:format=b,file=f2 858524命令,将堆信息导出到f2文件,然后在MAT中打开分析:

查看histogram和dominator_tree找出是线程池挂起的问题,去代码中搜索线程池找到问题

 

实际的代码:

下面是模拟的代码,实际的代码是订单查询过慢,因为调用了其它的微信服务,查用户头像,调用了用户服务查用户昵称,调用了分销系统,查询订单分成,所以当时是用countdownlatch,发起多个线程,并发的去其它服务里查询,但忘记关闭线程池导致,模拟代码如下:

@RestController("testController")
@RequestMapping(UserClient+"/h5/test")
@Slf4j
public class TestController extends BaseController implements AuthCookieAware {


    @GetMapping("/test1")
    public Response test1() throws Exception{

        int n = 300000;
        for (int i = 0; i < n; i++) {
            test2();
        }
        return new Response().ok();
    }

    public Response test2() throws Exception{

        ThreadPoolExecutor executor = new ThreadPoolExecutor(10,20,0, TimeUnit.SECONDS,new LinkedBlockingDeque<>(), new NamedThreadFactory("并发线程"));

        RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
        String jvmName = runtimeBean.getName();
        System.out.println("JVM Name = " + jvmName);    long pid = Long.valueOf(jvmName.split("@")[0]);
        System.out.println("JVM PID  = " + pid);
        ThreadMXBean bean = ManagementFactory.getThreadMXBean();

        for(int j = 0; j<10; j++){
            executor.execute(()->{
                System.out.println("当前线程总数为:"+bean.getThreadCount());
            });
        }
            //executor.shutdown();

        Thread.sleep(1000);
        System.out.println("线程总数为 = " + bean.getThreadCount());

        return new Response().ok();
    }

    public class NamedThreadFactory implements ThreadFactory {

        private final AtomicInteger poolNumber = new AtomicInteger(1);

        private final ThreadGroup threadGroup;

        private final AtomicInteger threadNumber = new AtomicInteger(1);

        public  final String namePrefix;

        NamedThreadFactory(String name){
            SecurityManager s = System.getSecurityManager();
            threadGroup = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup();
            if (null==name || "".equals(name.trim())){
                name = "pool";
            }
            namePrefix = name +"-"+
                    poolNumber.getAndIncrement() +
                    "-thread-";
        }

        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(threadGroup, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }
}

原因:线程池在使用完毕后未shutdown(),每次请求过来均会重新新建线程池。

解决方案:线程池使用完毕后要关闭,线程池最好设计成单例模式,线程池最好要命名,方便排查问题。

延伸:单例的线程池:

public class ThreadTool {

    public final static int corePoolSize = 2;
    public final static int maximumPoolSize = Runtime.getRuntime().availableProcessors() * 2 + 1;
    public final static int keepAliveTime = 60;
    public final static TimeUnit unit = TimeUnit.SECONDS;
    public final static BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(
            200);

    private static class Assistant {
        public final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    public static ThreadPoolExecutor getFixedThreadPool() {
        return Assistant.poolExecutor;
    }
}

延伸:为什么未关闭线程池会导致内存溢出

ThreadPoolExecutor的通用构造函数如下

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) { ... }


我们首先简单了解以上参数相关的规则

1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行
3.当workQueue已满,且maximumPoolSize>corePoolSize时,新提交任务会创建新线程执行任务
4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理
5.当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
6.当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
可以看出,allowCoreThreadTimeOut这个方法就像其字面的意思一样,允许Core Thread超时后可以关闭。

要想使线程池没有任务时销毁核心线程,需要启用allowCoreThreadTimeOut(true)同时将core size设置为0,而实际上,core size设置成任意一个正数值就可以,设置成0时,加不加allowCoreThreadTimeOut(true)都没有影响,因为这个方法是对core thread产生影响,但此时core thread为0,而且当新任务进来时,必须等到workQueue满时才会创建新线程,这也不是我们想要的结果,所以日常中不要将核心线程数设置成0,这样的话只有队列满了才会创建线程执行你的任务。
 

参考文档:https://blog.csdn.net/qq_24082175/article/details/81019490

参考文档:https://www.jianshu.com/p/1d2addd892fa

参考文档:https://blog.csdn.net/weixin_42008012/article/details/104932588

参考文档:https://blog.csdn.net/Evelyn_Jone/article/details/83068524?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.nonecase

 

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值