Tag Archives: jetty

ClassLoader内存溢出-实例分析

本文地址:http://nius.me/classloader-me…-leak-in-cases/

续上篇:ClassLoader内存溢出-从tomcat的reload说起

上一篇简要分析了内存溢出的原理,并提供了Mattias大神写的防御插件。Mattias大神还写了一篇非常手把手的教程Classloader leaks I – How to find classloader leaks with Eclipse Memory Analyser (MAT)教大家如何追踪ClassLoader内存溢出。

不过Mattias的插件也并不能适用所有的场合。比如我实验室遗留下来的java web项目(%>_<%)。只能自己上了!

原理回顾

回顾一下ClassLoader内存溢出的原理:

  1. web容器,一般会用WebAppClassLoader来加载其中一个app的所有类。
  2. 但是在启动web容器的时候,很多类是在其他ClassLoader(就是WebAppClassLoader被实例化之前)那加载的,比如jre的类,使用的一些库的类。注意,这里的加载,仅仅是加载类对象,例如加载了AppContext这个类,但是并不代表这个类已经有实例化的对象了。仅仅是一个Class对象存在。
  3. 等到WebAppClassLoader加载完需要的类之后,就开始启动web app,实例化servlet、spring等等这些东西,这时候,如果存在一个由其他ClassLoader加载的类,实例化了某一个对象,这个对象又保持了当前的WebAppClassLoader对象的引用,就造成了溢出。
  4. 因为在gc的时候,因为一些原因,例如单例模式,让其他ClassLoader加载的类的对象没有被回收,于是WebAppClassLoader就不会被回收。
  5. 进一步导致,由WebAppClassLoader加载的类对象无法回收,但是这些类实例化的对象可能会被回收。类对象包括这个类的静态成员,所以这些东西都进入了Perm区。

我们要做的,就是把捣蛋的对象找到,对症下药。

1. Proxool没有正确关闭

Proxool是众多数据库中连接池中口碑最好的一个。虽然很多人认为应该把连接池这种杂事交给web容器。在Spring中配置的Proxool一般如下:

<bean id="dataSource" class="org.logicalcobwebs.proxool.ProxoolDataSource">
        <property name="alias" value="proxoolDataSource" />
        <property name="driver" value="${connection.driver_class}" />
        <property name="driverUrl" value="${connection.url}" />
        <property name="user" value="${connection.username}" />
        <property name="password" value="${connection.password}" />
        <property name="houseKeepingSleepTime" value="${connection.housekeepingsleeptime}" />
        <property name="prototypeCount" value="${connection.prototypecount}" />
        <property name="maximumActiveTime" value="${connection.maximumactivetime}" />
        <property name="statistics" value="${proxool.statistics}" />
        <property name="simultaneousBuildThrottle" value="${proxool.simultaneous.build.throttle}" />
        <property name="maximumConnectionCount" value="${proxool.maximum.connection.count}" />
        <property name="minimumConnectionCount" value="${proxool.minimum.connection.count}" />
    </bean>

 

注意!这个bean没有destroy-method!连接池不会自动关闭!对于实验室的demo型项目,其实我也理解为啥毕业的师兄们都不管这些事。。但是,Proxool的DataSource类没有close方法。囧。中文社区有很多奇葩硬是在bean上加上destroy-method="close",这只能让spring报错,还是没有释放连接池。

于是,大杀器:写一个ServletContextListener

    public void contextDestroyed(ServletContextEvent event) {
        ProxoolFacade.shutdown(1000);
    }

 

然后在web.xml中注册这个listener

 <listener>
        <listener-class>me.nius.mlp.ProxoolShutdownListener</listener-class>
    </listener>

 

嗯。再trace一下,果然对WebAppClassLoader的引用少了Proxool,不过还有一大票其他的=。=

2. jdbc drivers没有取消注册

首先,要使用jdbc的驱动都会到java.sql.DriverManager那注册。当程序reload的时候,应该要deregister。当然tomcat会帮你做这件事,如果你自己没做,在reload的时候,会收到一个warn,说上一次没取消注册,为了防止memory leak,帮你deregister一下。

解决方案同上,写一个ServletContextListener,在contextDestroyed方法里做一下就好了。

3. mysql jdbc driver的AbandonedConnectionCleanupThread

解决了上面的问题,发现居然还有跟jdbc有关的引用。这个好像是mysql专有,不知道oracle或者其他数据库的驱动有没有类似问题。根源在于AbandonedConnectionCleanupThread。这个类看起来是用来清除一些没用的连接的,但是好心办了坏事,在reload的时候,这个对象不会被回收,而且保持了对WebAppClassLoader的引用!

同理,写个方法放到ServletContextListener里

 void shutdownCleanupThread(){
        try {
            AbandonedConnectionCleanupThread.shutdown();
        } catch (InterruptedException e) {
            logger.warn("SEVERE problem cleaning up: " + e.getMessage());
            e.printStackTrace();
        }
    }

 

这个似乎是mysql驱动的一个bug:http://bugs.mysql.com/bug.php?id=69526

4. sun.awt.AppContext

这个是最经典的。很多网上的case study都是讲的这个。这个AppContext是个单例模式,而且是jre的库,所以这个类早早就加载了。但是并没有用到。之后,程序中使用到了这个,调用AppContext.getInstance(),此时这个类保存了当前的ClassLoader的引用,就是WebAppClassLoader!

这个的解决方案跟前面的不同,前面都是关闭的时候把那些对象销毁,这里要在系统加载的时候,提前调用AppContext.getInstance(),而且要用root ClassLoader来干这件事。

     /**
     * Init AppContext using root ClassLoader to prevent memory leak
     */
    @Override
    public void contextInitialized(ServletContextEvent event) {
        try {
             final ClassLoader active = Thread.currentThread().getContextClassLoader();
             try {
              //Find the root classloader
              ClassLoader root = active;
              while (root.getParent() != null) {
               root = root.getParent();
              }
              //Temporarily make the root class loader the active class loader
              Thread.currentThread().setContextClassLoader(root);
              //Force the AppContext singleton to be created and initialized
              sun.awt.AppContext.getAppContext();
             } finally {
              //restore the class loader
              Thread.currentThread().setContextClassLoader(active);   
             }
            } catch ( Throwable t) {
             //Carry on if we get an error
            }
    }

 

注意,这里并不是实现ServletContextListener的contextDestroyed方法,而是contextInitialized方法。

Maven jetty plugin 9.1.0的bug

解决了上面的问题,发现用jetty跑,还是没法销毁,而保存WebAppClassLoader的对象,正式Maven jetty plugin里面的对象!这时用tomcat跑已经正常了,找不到对其的引用了,虽然还在内存里(因为java 7回收比较懒)。

仔细看了一下,jetty插件注册了一个ApplicationShutdownHook,这玩意是用在jvm退出的时候干一些释放资源的活用的。这个Hook里有一个org.eclipse.jetty.util.thread.ShutdownThread,这玩意保存了所有的WebAppClassLoader的引用!这不是搞笑么!居然被我发现了一个bug!

更新了一下插件版本到9.2.0,唔,fix了,确实是bug。头一次领悟到一个google不到的bug,想想还有点小激动呢。

这样一来,实验室的项目总算服服帖帖的能够正常reload了,再也不用一次次重启tomcat了,内牛满面啊。

Tomcat/Jetty Memory Leak Prevention

ClassLoader内存溢出是一个古老的话题,厂商怎么可能不重视!诺:

tomcat MemoryLeakProtection
Jetty Howto Prevent Memory Leaks

web容器早就备好了各种武器供大家使用啦!实际上大家可能用了都没意识到,因为避免了,所以不会溢出了= =!

不过,还是会出现遇到容器没法解决的情况,这时候,就得自己上手了!就我个人的使用体验来看,MATVisualVM好用,再次推荐Mattias的手把手教程:Classloader leaks I – How to find classloader leaks with Eclipse Memory Analyser (MAT)