返回renhui博客列表

classloader内存泄露的总结

发布于 7月前
摘要
classloader 内存泄露 垃圾回收机制

    最近一个月都在搞classloader内存泄露的问题,事情起因是苏州银行通过控制台起停项目,每启动一次内存都会蹭蹭往上涨,即使通过jconsole、jvisualVm主动触发垃圾回收机制都不能回收内存,这样多启动几次系统就报内存溢出的错误。

    下面我来分享下最近工作的经验,首先我们来了解什么是classloader内存泄露,所谓的classloader内存泄漏,举个简单例子说明一下。 假设有A和B两个classloader,分别加载 A 和 B两个类。 他们new出来的instance,取名叫 a 和 b。此时,如果你通过某种方式,将a的instance传递给b,b又不小心hold住a的实例时,就有可能发生memory leak。 仔细分析,主要是因为a实例hold class A , class A hold classloader A,b hold a,那么b也就跟classload A有了间接引用关系。 GC过程中,如果发现classloader a 从classloader b的引用关系可达(reached),那么classloader A是不会被回收。

我们来看具体的例子,以Servlet为例,看一下内存引用图。

public class Servlet1 extends HttpServlet {
  private static final String STATICNAME = "Simple";
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
 }
}

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

yQZ3U3.png

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

1、 像每个对象一样,Servlet1实例引用了其class对象

2、 每一个class对象都引用了加载它的classloader对象 
   3、 每一个classloader对象都持有着所有由它加载的类对象

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

mUjiQn.png

正如图所示,所有的类对象都是无法到达的,因此这些对象都将被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大的。我们看下对应的内存引用图:

jemQZf.png

从这个图片你可以看到一些意外的结果,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。那么,现在当该应用被销毁时,会发生什么?

viYNJ3.png

只有LeakServlet对象可以被gc回收。因为AppClassloader之外的classloader对象引用了CUSTOMLEVEL,导致CUSTOMLEVEL匿名类无法被gc回收,间接导致AppClassLoader也不能被gc回收,最终导致所有被AppClassLoader加载的类都无法被gc回收。

总结:如果由一个classloader加载的对象被另一个classloader加载的对象引用,可能会引起classloader内存泄露。

那么有哪些情况能引起classloader内存泄露问题呢?

1、应用加载器打开的线程未关闭

2、应用加载器关联的ThreadLocale未释放

3、其他加载器引用了应用加载器的实例

    知道了classloader内存泄露引起的大致原因,那么在实际应用中如何解决呢?下面我以tiny框架的websample项目作为示例,来讲解下classloader内存泄露问题的排除过程。

    从tomcat6.0.25开始,就有了"Find Leaks"按钮,用于检测应用是否存在classloader泄露的功能。首先在tomcat8下启动应用,然后通过控制台关闭应用,然后查看控制台日志,如果日志信息提示某个线程未关闭,或者ThreadLocal绑定的实现未释放,就说明应用存在classloader内存泄露问题,然后逐一解决问题,如果实在解决不了,比如是第三方包引入的问题,并且不能通过升级版本来解决的,可以借助Mattias Jiderhamn大神开发的"classloader-leak-prevention"来解决这些问题。

首先项目中依赖

<dependency>
<groupId>se.jiderhamn.classloader-leak-prevention</groupId>
<artifactId>classloader-leak-prevention-servlet</artifactId>
<version>2.1.0</version>
</dependency>

然后在web.xml中定义ClassLoaderLeakPreventorListener监听器

 <listener>
<listener-class>se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorListener</listener-class>
</listener>

点击"Find Leaks"按钮,message提示“No web applications appear to have triggered a memory leak on stop, reload or undeploy.”说明应用不存在classloader内存泄露问题,如果不是上面的提示信息,可以通过jconsole或者jvisualvm主动触发垃圾回收,然后再点击"Find Leaks"按钮,查看message提示信息。

在tomcat8下classloader内存泄露排查成功后,同样的工程在tomcat6下运行,通过控制台关闭应用后发现存在内存泄露问题,然后通过MAT进行分析,推荐Mattias的手把手教程: Classloader leaks I – How to find classloader leaks with Eclipse Memory Analyser (MAT) 。

1、File菜单下选择Acquire Heap Dump,如图所示:

blob.png

2、选择要监控的java进程,这里选择的是Bootstrap,然后点击Finish按钮

blob.png

3、继续点击Finish按钮

blob.png

4、由于检查的classloader内存泄露问题,按照下图所示打开 “Class Loader Explorer”视图

blob.png

5、右击应用加载器选择Class Loader----Path To GC Roots---exclude phantom/weak references

blob.png

得到的结果如下:

blob.png

从上面发现主线程持有com.ctc.wstx.io.BufferRecycler对象的SoftReference引用,导致WebappClassLoader加载的类以及类实例都不能释放。

为什么SoftReference引用的对象不能被垃圾回收呢?先来看看SoftReference的定义:

SoftReference 类的一个典型用途就是用于内存敏感的高速缓存。 SoftReference 的原理是:在保持对对象的引用时保证在 JVM 报告内存不足情况之前将清除所有的软引用。关键之处在于,垃圾收集器在运行时可能会(也可能不会)释放软可及对象。对象是否被释放取决于垃圾收集器的算法以及垃圾收集器运行时可用的内存数量。也就是说它不一定会被垃圾收集器回收,跟具体的垃圾收集器的算法以及垃圾收集器运行时可用的内存数量有关。

解决SoftReference引用对象被回收的问题,可以通过设置jvm参数来解决,比如在tomcat启动的时候增加“-XX:SoftRefLRUPolicyMSPerMB=1”参数即可。

加完jvm参数重新启动websample应用,然后通过控制台关闭应用,点击"Find Leaks"按钮,提示信息再次出现“No web applications appear to have triggered a memory leak on stop, reload or undeploy.” 至此websample应用的classloader内存泄露问题已经排除完毕。


 
相关信息
     标签
       附件
      文件 标题 创建者

      评分 0次评分

      日程