原创

一次tomcat内存溢出的排错经历《二》

上篇文章粗略的写了解决问题的大致过程。这次使用jvisualvm.exe,来还原程序出错的具体过程。
jvisualvm.exe是JDK自带的工具,在JDK的bin目录下可以直接找到,双击打开即可,界面如下:
file

上次发现是AsyncManager$1这个类存在问题,从类名上看这是个匿名内部类,可以发现主类是AsyncManager
AsyncManager.java

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

public class AsyncManager {

    //单例模式   设计模式
    private AsyncManager(){

    }
    //内存中 只有 一个 AsyncManager对象存在

    private final static AsyncManager asyncManager = new AsyncManager();

    public static AsyncManager getInstance(){
        return asyncManager;
    }


    //声明 一个变量
    public   ExecutorService  executors;


    public void execute(Runnable rb){
        executors.execute(rb);
    }

    //初始化的方法  初始化的时候  规定好  有几个线程 产生
    public void initWith(int threadCount){
        //ThreadFactory  线程工厂  负责 生成线程
        this.executors = Executors.newFixedThreadPool(threadCount, new ThreadFactory() {
            //初始化 线程组
            ThreadGroup tg = new ThreadGroup("AsyncManager");

            @Override
            public Thread newThread(Runnable r) {
                //Thread t = new Thread(tg,r);
                return new Thread(tg,r);
            }
        });
    }        
}

使用全局搜索找到使用这个类的代码
file
发现调用的方式是这样的。

AsyncManager.getInstance().initWith(50);
AsyncManager.getInstance().execute(new Runnable(){})

根据业务,这部分的代码会经常调用,我们来模拟

import java.util.Timer;
import java.util.TimerTask;

import com.dragonsoft.commons.terminalPassageway.AsyncManager;
import com.sun.management.OperatingSystemMXBean;

public class ThreadTest {
  public static void main(String[] args) {
      Timer timer = new Timer();
      //使用定时器来模拟请求调用改代码
      timer.schedule(new TimerTask() {
              public void run() {                  
                  AsyncManager.getInstance().initWith(50);
                  AsyncManager.getInstance().execute(new Runnable(){
                        @Override
                        public void run(){
                            System.out.println(1);
                        }

                    });

              }
      }, 2000 , 1000);           
}}

运行Main方法后,我们打开之前说的软件,就会看到进程了,双击打开
file
这是与线程有关的类,所以我们直接去看线程相关信息。
file
通过动图,我们了解到实时线程一直在在创建,按道理说线程生命周期是执行完之后,就会消亡。我们这里的线程的任务是输出1即可。所有没有别的任务。
但是不停的创建线程就有问题了。仔细观察,执行完任务的线程都处于等待状态,而不是消亡。问题的真相越来越近了。
观察代码创建线程池的代码是这句

    this.executors = Executors.newFixedThreadPool();

查看JDK文档,关于newFixedThreadPool的解释如下

创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

也就是说,创建完这个线程后不会自动消失,而是一直处于等待状态,也就很好的解释刚才的那种现象了。
因此就有了下面的解决方案。
参考地址:https://blog.csdn.net/clementad/article/details/75663956
当然,上面的解决方案是我在解决完问题之后和同事交流得到的。

自己在看到这种现象之后,首先想到的是执行完线程之后关闭不就解决了,因此查看这个类的源码
file
嘿嘿,找到了。也就是shutdown方法,加进去,看看效果
file

题外话(如不了解,自行百度。看不懂英文不怪我,要是懂英文直接查看源码文档是最好的解释!):
file

加上上面的这句话,继续执行方法:
file
从控制台可以看到任务一直在执行,但是线程却没有一直增加。问题完美解决。

另一种解决方案:
file

也就是上面博客提到的解决方案,自我感觉第二种解决方案会更好,初始化一次线程池,重复利用就行了,这样可以大量的减少系统的开销没节约了线程创建,消亡的步骤,想想Spring容器的设计理念,不正是这样的吗?还有某某连接池,都是这种设计。

正文到此结束(点击广告是对作者最大的支持)