Classloader内存泄露

在你从新部署你的应用程序到应用服务器(好比tomcat、weblogic等)时,你是否也遇到过 java.lang.OutOfMemoryError:PermGen space error? 是否也曾一边抱怨这个应用服务器,一边重启,而后继续你的工做,同时脑子里还在想着这必定是该服务器的一个BUG。那些应用服务器开发者们,应该仔细一点,对吗?或许吧,可是你有想过,这的的确确是你的过错吗?html

咱们先看一下下面这段代码,看似没有任何问题的一个Servlet类:java

public class MyServlet extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// Log at a custom level
Level customLevel = new Level("OOPS", 555) {
};
Logger.getLogger("test").log(customLevel, "doGet() called");
}
}

试着重复发布这个例子几回,我敢打赌最终你会看到java.lang.OutOfMemoryError: PermGen space error.若是你想知道发生了什么,请继续往下看。web

问题描述:缓存

使用应用程序服务器(好比Glassfish、Tomcat等),咱们能够同时部署多个应用程序。而应用的开发老是迭代进行的,添加或者改变现有的代码可能像屡见不鲜同样。而后,为了测试新的改动,你从新编译,而后从新部署,而不影响其余已经发布的应用程序(这种方式称之为热部署),由于不用重启应用服务器。这种热部署的机制不少应用服务器都支持(好比Glassfish,Tomcat等)。tomcat

而热部署的实现方式,就是使用不一样的classloader去加载每个应用程序。简单地说,一个classloader就是一个从jar文件中加载.class文件的简单的类。当你卸载应用时,该classloader连同全部由该classloader加载的类都将被垃圾回收掉(可能不会当即回收,可是没用任何引用的对象,最终都会被gc回收)。服务器

可是,有时候有些对象会防不胜防地引用到classloader,这样gc就没法对其进行回收。这就是java.lang.OutOfMemoryError:PermGen space error 的由来。app

永久区框架

什么是永久区?java虚拟机中的内存被分为几个部分,其中一个部分被称之为永久区,或者方法区。这个区域是用来加载类文件的。这个区域的大小在JVM中是固定的,当JVM运行以后,该区域大小不会改变。你能够经过-XX:MaxPermSize参数来指定该区域的大小。在Sun 的HotSpot虚拟机里,该值默认是64MB函数

若是存在classloader泄露,而你又常常加载新的类,那么最终这块区域将被使用完,尽管整个堆被占用的不多。就算你使用了-Xmx参数也无济于事,由于该参数只会影响整个堆的大小,可是不会影响该方法区的大小。测试

好比以下这段简短的代码:

private void x1() {
for (;;) {
List c = new ArrayList();
}
}

这段代码持续地分配内存,可是这个应用并不会内存溢出。那是由于这些被建立的对象都是能够被垃圾回收的,当没有足够的内存去建立新的对象时,gc将会对那些死对象(没有被任何对象引用的对象(s))进行回收。

首先,咱们再次简化上面Servlet,看一下内存引用图。

public class Servlet1 extends HttpServlet {
private static final String STATICNAME = "Simple";

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}
}

当上面的Servlet被加载以后,内存中将会有以下对象:

图中被application classloader加载的类用黄色标识,其他的使用绿色能够看到,一个容器对象(Container)引用了两个对象,一个是用于加载该应用程序的classloader,还有一个引用到了Servlet1(主要为了当有web请求进来的时候,能够执行doGet()方法)须要注意的是,STAtICNAME 对象是被Servlet1的class对象持有的。其余须要注意的是:

一、 像每一个对象同样,Servlet1实例引用了其class对象 
二、 每个class对象都引用了加载它的classloader对象 
三、 每个classloader对象都持有着全部由它加载的类对象

这里一个重要的结果是:若是其余classloader加载的对象引用了由AppClassLoader加载的对象,那么全部由AppClassLoader加载的类都将没法被gc回收。当应用程序被销毁的时候,容器对象(Container)取消对Servlet1和AppClassLoader的引用。这个时候的内存引用图以下:

正如图所示,全部的类对象都是没法到达的,所以这些对象都将被gc回收。如今咱们看一下,使用最上面的那个例子会发生什么。

public class LeakServlet extends HttpServlet {
private static final String STATICNAME = "This leaks!";
private static final Level CUSTOMLEVEL = new Level("test", 550) {
}; // anon class!

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Logger.getLogger("test").log(CUSTOMLEVEL, "doGet called");
}
}

请注意,CUSTOMLEVEL的类是一个匿名类,这是由于Level的构造函数是protected大的。咱们看下对应的内存引用图:

从这个图片你能够看到一些意外的结果,Level 类引用了全部被建立的Level实例。JDK中Level的构造函数以下:

protected Level(String name, int value, String resourceBundleName) {
if (name == null) {
throw new NullPointerException();
}
this.name = name;
this.value = value;
this.resourceBundleName = resourceBundleName;
synchronized (Level.class) {
known.add(this);
}
}

其中,known是Level中的一个静态的ArrayList。那么,如今当该应用被销毁时,会发生什么?

只有LeakServlet对象能够被gc回收。由于AppClassloader以外的对象引用了CUSTOMLEVEL,致使CUSTOMLEVEL匿名类没法被gc回收,间接致使AppClassLoader也不能被gc回收,最终致使全部被AppClassLoader加载的类都没法被gc回收。

总结:若是由一个classloader加载的对象被另外一个classloader加载的对象引用,可能会引发classloader内存泄露。

为何classloader内存泄露值得注意:

一、若是一个classloader存在内存泄露,那么它将会一直持有其加载的全部类对象,而每一个类对象又持有了其全部静态变量。而在通常的应用程序当中,静态对象中经常维护了对象的缓存,单例对象以及各类配置和应用程序状态等数据。即便,在你的应用中也许没有任何静态的缓存,那也不意味着你使用的框架以及一些第三方资源不会这么作。所以,classloader内存泄露,致使的后果是每每是很惨重的。

二、那么classloader内存泄露,很难发生吗?错。一不当心引用了一个由另外一个classloader加载的对象,就会致使classloader内存泄露。尽管这个对象彷佛是无害的,可是,它依然维持了classloader的引用和全部相关的应用程序数据。应用中一个这样的误操做可能将会致使最终的java.lang.OutOfMemoryError:PermGen space error 
因此,classloader内存泄露,很容易发生。

英文地址: http://frankkieviet.blogspot.com/2006/10/classloader-leaks-dreaded-permgen-space.html

转载自:http://www.tuicool.com/articles/eAnayu