`
timen_zbt
  • 浏览: 76349 次
社区版块
存档分类
最新评论

Spring,hibernate,struts2(SSH)项目在tomcat中多次reload时出现OutOfMemoryError:PermGen Space

阅读更多
(此文章转自网上)
我做的应用是以Spring为系统的基础框架,mysql为后台数据库.tomcat上发布后,总是不能进行热部署(reload),多次reload,就会出OutOfMemory PermGen,

为此烦恼了很久,总于下定决心找找根源.
经过3天的不懈努力,小有成果,记录下来

实际上下面的分析都已经没什么用了,如果你使用tomcat6.0.26及以后的版本,我所说的这些情况都已经被处理了,并且比我处理的还要多很多.可以下载tomcat6.0.26的源代码
看看WebappClassLoader类的处理就成了.

通过分析工具的分析(用了YourKit,以及JDK1.6/bin下的jps/jmap/jhat),发现有下面几个方面会造成memory leak.

1.SystemClassLoaderWebappClassLoader加载的类相互引用,tomcat reload只是卸载WebappClassloader中的class,SystemClassLoader是不会卸载的(否则其他应用也停止了).但是WebappClassloader加载的类被SystemClassLoader引用的化,WebappClassloader中的相关类就不会被JVM进行垃圾收集

目前发现2种容易产生这种leak的现象.
a.
在使用java.lang.ThreadLocal的时候很容易产生这种情况
b.
使用jdbc驱动,而且不是在tomcat中配置的公共连接池.java.sql.DriverManager一定会产生这种现象


ThreadLocal.set(Object),
如果这个ObjectWebappsClassLoader加载的,使用之后没有做ThreadLocal.set(null)或者ThreadLocal.remove(),就会产生memory leak.
由于ThreadLocal实际上操作的是java.lang.Thread类中的ThreadLocalMap,Thread类是由SystemClassLoder加载的.而这个线程实例(main thread)tomcat reload的时候不会销毁重建,必然就产生了SystemClassLoder中的类引用WebappsClassLoader的类.

DriverManager也是由SystemClassLoder载入的,当初始化某个JDBC驱动的时候,会向DriverManager中注册该驱动,通常是***.driver,例如com.mysql.jdbc.Driver
这个Driver是通过class.forName()加载的,通常也是加载到WebappClassLoader.这就出现了两个classLoader中的类的交叉引用.导致memory leak.

解决办法:
写一个ServletContextListener,contextDestroyed方法中统一删除当前ThreadThreadLocalMap中的内容.
public class ApplicationCleanListener implements ServletContextListener {

public void contextInitialized(ServletContextEvent event) {
}

public void contextDestroyed(ServletContextEvent event) {
         //
处理ThreadLocal
   ThreadLocalCleanUtil.clearThreadLocals();

   /*
   *
如果数据故驱动是通过应用服务器(tomcat etc...)中配置的<公用>连接池,这里不需要否则必须卸载Driver
   * 
   *
原因: DriverManagerSystem classloader加载的, Driverwebappclassloader加载的,
   * driver
保存在DriverManager,reload过程中,由于system
   * classloader
不会销毁,driverManager就一直保持着对driver的引用,
   * driver
无法卸载,driver关联的其他类
   * ,
例如DataSource,jdbcTemplate,dao,service....都无法卸载
   */
   try {
    System.out.println("clean jdbc Driver......");
    for (Enumeration e = DriverManager.getDrivers(); e
      .hasMoreElements();) {
     Driver driver = (Driver) e.nextElement();
     if (driver.getClass().getClassLoader() == getClass()
       .getClassLoader()) {
      DriverManager.deregisterDriver(driver);
     }
    }

   } catch (Exception e) {
    System.out
      .println("Exception cleaning up java.sql.DriverManager's driver: "
        + e.getMessage());
   }


}

}


/**
*
这个类根据
*/
public class ThreadLocalCleanUtil {

/**
*
得到当前线程组中的所有线程 description:
* 
* @return
*/
private static Thread[] getThreads() {
   ThreadGroup tg = Thread.currentThread().getThreadGroup();

   while (tg.getParent() != null) {
    tg = tg.getParent();
   }

   int threadCountGuess = tg.activeCount() + 50;
   Thread[] threads = new Thread[threadCountGuess];
   int threadCountActual = tg.enumerate(threads);

   while (threadCountActual == threadCountGuess) {
    threadCountGuess *= 2;
    threads = new Thread[threadCountGuess];

    threadCountActual = tg.enumerate(threads);
   }

   return threads;
}

public static void clearThreadLocals() {
   ClassLoader classloader = Thread
     .currentThread()
     .getContextClassLoader();

   Thread[] threads = getThreads();
   try {
    Field threadLocalsField = Thread.class
      .getDeclaredField("threadLocals");

    threadLocalsField.setAccessible(true);
    Field inheritableThreadLocalsField = Thread.class
      .getDeclaredField("inheritableThreadLocals");

    inheritableThreadLocalsField.setAccessible(true);

    Class tlmClass = Class
      .forName("java.lang.ThreadLocal$ThreadLocalMap");

    Field tableField = tlmClass.getDeclaredField("table");
    tableField.setAccessible(true);

    for (int i = 0; i < threads.length; ++i) {
     if (threads[i] == null)
      continue;
     Object threadLocalMap = threadLocalsField.get(threads[i]);
     clearThreadLocalMap(threadLocalMap, tableField, classloader);

     threadLocalMap = inheritableThreadLocalsField.get(threads[i]);

     clearThreadLocalMap(threadLocalMap, tableField, classloader);
    }
   } catch (Exception e) {

    e.printStackTrace();
   }
}

private static void clearThreadLocalMap(Object map,
    Field internalTableField, ClassLoader classloader)
    throws NoSuchMethodException, IllegalAccessException,
    NoSuchFieldException, InvocationTargetException {
   if (map != null) {
    Method mapRemove = map.getClass().getDeclaredMethod("remove",
      new Class[] { ThreadLocal.class });

    mapRemove.setAccessible(true);
    Object[] table = (Object[]) internalTableField.get(map);
    int staleEntriesCount = 0;
    if (table != null) {
     for (int j = 0; j < table.length; ++j) {
      if (table[j] != null) {
       boolean remove = false;

       Object key = ((Reference) table[j]).get();
       if ((key != null)
         && (key.getClass().getClassLoader() == classloader)) {
        remove = true;

        System.out.println("clean threadLocal key,class="
          + key.getClass().getCanonicalName()
          + ",value=" + key.toString());
       }

       Field valueField = table[j]
         .getClass()
         .getDeclaredField("value");

       valueField.setAccessible(true);
       Object value = valueField.get(table[j]);

       if ((value != null)
         && (value.getClass().getClassLoader() == classloader)) {
        remove = true;
        System.out.println("clean threadLocal value,class="
          + value.getClass().getCanonicalName()
          + ",value=" + value.toString());

       }

       if (remove) {

        if (key == null)
         ++staleEntriesCount;
        else {
         mapRemove.invoke(map, new Object[] { key });
        }
       }
      }
     }
    }
    if (staleEntriesCount > 0) {
     Method mapRemoveStale = map
       .getClass()
       .getDeclaredMethod("expungeStaleEntries", new Class[0]);

     mapRemoveStale.setAccessible(true);
     mapRemoveStale.invoke(map, new Object[0]);
    }
   }
}
}

2.对于使用mysql JDBC驱动的:mysql JDBC驱动会启动一个Timer Thread,这个线程在reload的时候也是无法自动销毁.
因此,需要强制结束这个timer

可以在上面的ApplicationCleanListener中加入如下代码:

   try {
    Class ConnectionImplClass = Thread
      .currentThread()
      .getContextClassLoader()
      .loadClass("com.mysql.jdbc.ConnectionImpl");
    if (ConnectionImplClass != null
      && ConnectionImplClass.getClassLoader() == getClass()
        .getClassLoader()) {
     System.out.println("clean mysql timer......");
     Field f = ConnectionImplClass.getDeclaredField("cancelTimer");
     f.setAccessible(true);
     Timer timer = (Timer) f.get(null);
     timer.cancel();
    }
   } catch (java.lang.ClassNotFoundException e1) {
    // do nothing
   } catch (Exception e) {
    System.out
      .println("Exception cleaning up MySQL cancellation timer: "
        + e.getMessage());
   }


3.common-logging+log4j
似乎也会导致leak,看网上有人说在ApplicationCleanListene6中加入这行代码就可以:
LogFactory.release(Thread.currentThread().getContextClassLoader());

我没试成功,懒得再找原因,直接换成了slf4j+logback,没有问题.据说slf4j+logback的性能还要更好.


后记:
tomcat-6.0.26
之前的版本(我用的是tomcat-6.0.18),加入上述ApplicationCleanListener,多次reload,不会出现outOfMemory.
但要注意,第一次启动后,reload一次,内存会增加,也就是看着还是由memory Leak,但是重复reload,内存始终保持在第一次reload的大小.似乎tomcat始终保留了双WebappClassLoader.因此,配置内存要小心些,至少要保证能够load两倍的你的所有jar包的大小(当然,是指Perm的内存大小).

测试过程中最好加上 JVM参数 -verbosegc,这样,在做GC的时候可以直观的看到class被卸载.

 

分享到:
评论

相关推荐

    解决struts2.1.6+spring+hibernate 中文乱码

    struts.i18n.reload=true #struts.locale=zh_CN struts.i18n.encoding=GBK ) web.xml(仅写出中文处理部分,spring的配置不用改变) &lt;filter-name&gt;encodingFilter &lt;filter-class&gt;org.springframework.web.filter...

    Struts2属性文件详解

    该属性指定是否允许在Struts 2标签中使用表达式语法,因为通常都需要在标签中使用表达式语法,故此属性应该设置为true,该属性的默认值是true. struts.devMode 该属性设置Struts 2应用是否使用开发模式.如果设置该属性...

    spring3.2+strut2+hibernate4

    spring3.2+strut2+hibernate4 注解方式。&lt;struts&gt; spring.xml &lt;beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi=...

    Struts课堂笔记.rar--struts2的struts.properties配置文件详解

    是否struts过滤器中提供的静态内容应该被浏览器缓存在头部属性中 struts.serve.static Whether the Struts filter should serve static content or not 是否struts过滤器应该提供静态内容 struts.tag....

    让MyEclipse里的Tomcat自动Reload

    让MyEclipse里的Tomcat自动Reload

    Struts2\constant应用

    该属性指定是否允许在Struts 2标签中使用表达式语法,因为通常都需要在标签中使用表达式语法,故此属性应该设置为true,该属性的默认值是true。 struts.devMode 该属性设置Struts 2应用是否使用开发模式。如果...

    解决Tomcat在修改代码后不会自动reload的问题

    1.找到使用的Tomcat安装目录...像这里也有Tomcat7的配置文件,之前网上有人说可以修改这里的内容,但是发布时用的是自己配置的tomcat,所以并不能解决问题。 您可能感兴趣的文章:解决Tomcat修改get提交请求乱码问题

    struts2配置2.5版

    &lt;constant name="struts.configuration.xml.reload" value="true" /&gt; 6.查看源码:Build path 后的类库中,奶瓶图标找到struts-core-2.5.16.jar 右键--&gt;properties--&gt;java Source Attachment--&gt;External ...

    vite-plugin-full-reload::recycling_symbol:修改文件后自动重新加载页面

    npm i -D vite-plugin-full-reload # yarn add -D vite-plugin-full-reload 用法 :rocket: 将其添加到vite.config.ts的插件 import { defineConfig } from 'vite' import FullReload from 'vite-plugin-full-reload...

    tomcat-7.0.52.tar.gz 【linux】

    安装tomcat 1.下载tomcat 2.上传到linux 3.新建一个文件夹 mkdir /usr/local/tomcat 4.移动或者复制 tomcat...tar.gz 到 /usr/local/tomcat mv apache-tomcat-7.0.52.tar.gz /usr/local/tomcat/ 5.进入/usr/...

    spring-boot-reference.pdf

    Spring Boot Documentation 1. About the Documentation 2. Getting Help 3. First Steps 4. Working with Spring Boot 5. Learning about Spring Boot Features 6. Moving to Production 7. Advanced Topics II. ...

    chrome_live_reload_bug:使用livereload时,chrome源地图中的bug的POC

    使用livereload时,chrome源地图中的错误的POC。 一些来源来自: : 提交的错误: : 本示例使用SASS代替LESS,因为它得到了更好的支持,并且更易于演示。 第1步:克隆整个仓库$ git clone ...

    FIR 高级应用 FIR Reload 在线重新载入系数的使用

    FIR 高级应用 FIR Reload 在线重新载入系数的使用 https://blog.csdn.net/qq_46621272/article/details/125348908 文章有该代码详细说明 https://blog.csdn.net/qq_46621272/article/details/125292610 FIR 使用...

    vue-hot-reload-api:HotVue组件的热重载API

    vue-hot-reload-api 注意: vue-hot-reload-api@2.x仅适用于vue@2.x Vue组件的热重装API。 这就是和在引擎盖下使用的东西。用法仅当您基于Vue组件编写一些构建工具链时,才使用此功能。 对于正常的应用程序使用,只...

    dword:Python中的Deep Word实用程序

    % autoreload 2 The autoreload extension is already loaded. To reload it, use: %reload_ext autoreload 希望您喜欢这个项目 安装 pip install dword 快速开始 您可以使用generate_video函数创建合成视频 ...

    CentOS调整SSH默认端口

    调整SSH默认端口 vim /etc/ssh/sshd_config 将默认配置中的port 22调整成**(截图见附件WORD) firewall-cmd --zone=public --add-port=**/tcp --permanent firewall-cmd --reload systemctl restart sshd 备注:...

    livereload:在前端&后端项目中使用livereload

    但是现在,通过创建代理服务器,我们可以在后端项目中使用livereload,比如PHP、Python、Ruby…… 演示 ( )安装其实这是一个node封装的模块你必须先 $ sudo npm install -g reload-man用法$ reload-man -D /var/www...

    crx-livereload::firecracker:Chrome Extension Live Reloader

    从无耻撕毁用法在manifest.json : " permissions " : [ " management " , " activeTab " ] management允许重新加载chrome扩展程序(必需) activeTab允许将状态记录到当前选项卡(可选) 在您的background脚本中:...

    cUrl-7.39.0

    CURLOPT_COOKIELIST: Added "RELOAD" command build: Added WinIDN build configuration options to Visual Studio projects ssh: improve key file search SSL: public key pinning. Use CURLOPT_PINNEDPUBLICKEY ...

Global site tag (gtag.js) - Google Analytics