这里记录一下遇到的一个tomcat memory leak的问题:
前置知识点
在类使用完之后,如果满足下面的情况,类就会被卸载:
1.该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收。
3.该类对应的java.lang.Class对象没有任何地方被引用,没有在任何地方通过反射访问该类的方法。
关于tomcat memory leak
其实tomcat的内存泄漏,根本原因是WebAppClassloader的强引用一直有效导致不能不被回收。一般有两种常见场景:
1.容器中创建的线程没有结束
If a webapp creates a thread, by default its context classloader is set to the one of the parent thread (the thread that created the new thread). In a webapp, this parent thread is one of tomcat worker threads, whose context classloader is set to the webapp classloader when it executes webapp code.
上面这句话的意思是说,当容器中的app创建对象时,对象的context classloader会被设置为父线程(创建本线程的线程)的class loader,通常是tomcat的worker线程,其classloader是WebAppClassloader。所以说容器中的线程,包括应用自身的线程,会持有WebAppClassloader强引用。当线程不能被正常销毁时,WebAppClassloader的强引用一直存在,WebAppClassloader不能被JVM回收,因此WebAppClassloader加载的所有类都不能被卸载,产生内存泄漏。
案例:
public class LeakingServlet extends HttpServlet {
private Thread leakingThread;
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
if (leakingThread == null) {
synchronized (this) {
if (leakingThread == null) {
leakingThread = new Thread("leakingThread") {
@Override
public void run() {
synchronized (this) {
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
leakingThread.setDaemon(true);
//leakingThread.setContextClassLoader(null);
leakingThread.start();
}
}
}
response.getWriter().println("Hello world!");
}
}
2.一些特殊class对象,不能被gc回收,且这些对象持有WebAppClassloader的强引用。
案例:
Webapp class instance as ThreadLocal value
Suppose that we have the following class in the common classpath (for instance in a jar in tomcat/lib) :
public class ThreadScopedHolder {
private final static ThreadLocal
And those 2 classes in the webapp :
public class MyCounter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
public class LeakingServlet extends HttpServlet {
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException {
MyCounter counter = (MyCounter)ThreadScopedHolder.getFromHolder();
if (counter == null) {
counter = new MyCounter();
ThreadScopedHolder.saveInHolder(counter);
}
response.getWriter().println(
"The current thread served this servlet " + counter.getCount()
+ " times");
counter.increment();
}
}
If the servlet is invoked at least once, the webapp classloader would not be GCed when the app is stopped: since the classloader of ThreadScopedHolder is the common classloader, it remains forever which is as expected. But its ThreadLocal instance has a value bound to it (for the non-terminated thread(s) that served the sevlet), which is an instance of a class loaded by the webapp classloader…
实验:
启动Tomcat容器,且容器webapp目录下部署有一个app,此时dump堆信息可以看到有两个WebAppClassloader对象。
其中#2是我们app的classloader,#1是忽略不看。
这里说下tomcat类加载器,会有一个父类加载器URLClassLoader去加在tomcat必须的jar包(catalina目录),这是一些在maven中作为provided的包,放在tomcat的lib下就会被这个loader加载。然后才是WebAppClassLoader去加载我们的app。
回归正题,我们开始实验。在启动之后我进行一次热部署,这时候控制台提示内存泄漏了,dump堆信息看下:
果然WebAppClassLoader多了一个,热部署时上一个classloader没有被回收,所以产生了内存泄漏。
前面说道两种webappclassloader不被回收的情况,我们先来看第一种,这里elastic job的线程是在以线程池的形式存在的,在我们自己的方法里加上这段代码来取消inner-job对webappclassloader的引用:
Thread t = Thread.currentThread();
if (null != t.getContextClassLoader()) {
t.setContextClassLoader(null);
}
按照上面的操作重复热部署,之后发现webappclassloader仍在增加,说明还是没有回收。首先这个一个耗时很长的job,热部署后原job线程仍在执行,容器上下文有一些对象和class没有回收,那么为什么webappclassloader没有被回收?
这事儿比较复杂,耐心做实验吧,仔细看下堆信息发现,WebAppClassloader最近的垃圾回收根节点是这个实例:
点击去看下:
到这里有必要在了解一下elastic job的线程池了。先上图:
图1:
elastic job 在启动的时候,对每一个job,首先会启动两个线程,一个scheduler调度线程和一个worker线程。然后在定时触发时,首次触发会新建一个线程池,以jobName为key缓存到map中,用户方法就是用线程池中的inner-job进行调度,这里我有五个分片,所以第二张图有5个inner-job.
而我们上一个实验中将context classloader设置为null,是对inner-job的。这里scheduler线程(位于调度线程池,线程池大小为1)仍然持有了webappclassloader的强引用。
所以说要想热部署,解决内存泄漏,靠开发者自身是不行的了,这里我使用的解决方案是在elastic job控制台将app下所有的任务都关掉,这时候调度线程池也会被shutdown,剩下的就是等执行缓慢的inner-job执行完本次用户任务退出后直接热部署即可,这样不会有内存泄漏发生。
解决方案是有了,但是理论上还有些东西没有弄清,是不是因为调度线程池持有了webappclassloader导致了内存泄漏,还是说同时也存在一些webappclassloader自身加载的class以及class相应对象扔存在导致不能被回收。
还是继续实验,在控制台关闭任务后,scheduler线程很快被回收,内存中运行时间较长(几分钟)的inner-job仍然在执行
这个时候进行热部署,如果webappclassloader仍未被回收,就不是elastic’job线程持有类加载器强引用导致,那就可以说明正在运行的inner-job线程拥有的对象和class持有类加载器引用,
且此强引用在gc可达性分析算法中是有效的!
即存在一个这样的引用链 thread–>unkonwn obj or class –>WebAppClassLoader,由于thread的有效性,导致类加载器加载的类和加载器本身的引用是有效的,即根节点可达。
继续实验,步骤:
1.启动tomcat,等待第一次job执行
2.job执行,此时在控制台挂关闭任务,此时dump线程信息发现scheduler线程已经被回收
3.执行热部署,然后dump堆信息
执行完上述操作,发现内存泄漏仍然存在,看下第3步dump的堆信息,发现确实WebAppClassLoader没有被回收,查看下classloader的垃圾回收根节点,如图:
com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread
线程的context classloader持有WebAppClassLoader的强引用。
这里说明仍然在执行的job影响了WebAppClassLoader的回收。即存在这样一条调用链:
inner-job–>User DefineServcie(用户代码)–>CreateConnectionThread–>WebAppClassLoader.
没办法,只能等这个job执行完退出后,再进行热部署吧。
参考:
https://stackoverflow.com/questions/17968803/threadlocal-memory-leak
https://wiki.apache.org/tomcat/MemoryLeakProtection
关于log4j由于ThreadLocal引发内存泄漏bug:
https://bz.apache.org/bugzilla/attachment.cgi?id=28091&action=edit
解决方案:每次用过之后,线程退出前finally中调用remove方法清除引用。