• ClassLoader引起内存泄漏问题


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

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

    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。

      问题描述:

      使用应用程序服务器(比如Glassfish、Tomcat等),我们可以同时部署多个应用程序。而应用的开发总是迭代进行的,添加或者改变现有的代码可能像家常便饭一样。然后,为了测试新的改动,你重新编译,然后重新部署,而不影响其他已经发布的应用程序(这种方式称之为热部署),因为不用重启应用服务器。这种热部署的机制很多应用服务器都支持(比如Glassfish,Tomcat等)。

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

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

      永久区

      什么是永久区?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对象持有的。其他需要注意的是:

      1、 像每个对象一样,Servlet1实例引用了其class对象 
      2、 每一个class对象都引用了加载它的classloader对象 
      3、 每一个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内存泄露值得注意:

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

    2、那么classloader内存泄露,很难发生吗?错。一不小心引用了一个由另一个classloader加载的对象,就会导致classloader内存泄露。尽管这个对象似乎是无害的,但是,它依然维持了classloader的引用和所有相关的应用程序数据。应用中一个这样的误操作可能将会导致最终的java.lang.OutOfMemoryError:PermGen space error 
    所以,classloader内存泄露,很容易发生。

  • 相关阅读:
    Java开发环境搭建(一)
    Android随笔之——Android广播机制Broadcast详解
    Ubuntu 14.04 LTS中怎样安装fcitx中文输入法
    Jenkins:容器化微服务持续集成-高配版
    Jenkins:容器化微服务持续集成-低配版
    JVM(1):JVM与Java体系结构
    RocketMQ:集群搭建
    RocketMQ:单机搭建
    Spring Cloud OAuth2:分布式认证授权
    Spring Security OAuth2:SSO单点登录
  • 原文地址:https://www.cnblogs.com/jing-yi/p/13223536.html
Copyright © 2020-2023  润新知