SSH-A web application created a ThreadLocal with key of type [null]

本文探讨了Tomcat6.025版本后引入的内存泄露检查机制,并提供了两种解决方案来应对这一问题:使用较低版本的Tomcat或关闭特定监听器。

A web application created a ThreadLocal with key of type [null]

类似错误:

引用文章

http://confluence.atlassian.com/pages/viewpage.action?pageId=218275753

大概是说tomcat 6.025之后引入了一种内存泄露的检查机制,会把不能垃圾收集的对像做日志。

第一种解决办法:

使用低于6版本的tomcat

第二种解决办法:

在tomcat的server.xml文件中把
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener"/>

这个监听给关了

 

 

<think>我们遇到了一个Tomcat的严重警告:SEVERE [RMI TCP Connection(2)-127.0.0.1] org.apache.catalina.loader.WebappClassLoaderBase.checkThreadLocalMapForLeaks ... 这个警告的意思是:Web应用程序[rf-wms-service]创建了一个ThreadLocal变量,其键为[java.lang.ThreadLocal]类型,值为[java.lang.Class]类型(具体是oracle.sql.TypeDescriptorFactory),但是在Web应用程序停止时未能将其移除。这可能会导致内存泄漏,因为ThreadLocal中保存的值可能会阻止Web应用程序的类加载器被垃圾回收,从而造成内存泄漏。 原因分析: 在Web应用程序停止时,Tomcat会检查是否有ThreadLocal变量没有被清理。ThreadLocal变量是线程局部的变量,每个线程都有一份独立的副本。如果ThreadLocal中保存了某个对象,而这个对象又是由Web应用程序的类加载器加载的,那么即使Web应用程序停止,这个对象也会一直存在于线程的ThreadLocalMap中,从而阻止Web应用程序的类加载器被回收,最终导致内存泄漏。 解决方法: 我们需要确保在Web应用程序停止时,清理掉所有由该应用程序创建的ThreadLocal变量。通常,我们可以使用ServletContextListener监听器,在contextDestroyed方法中清理ThreadLocal变量。 但是,请注意,这个ThreadLocal是由oracle.sql.TypeDescriptorFactory创建的,这可能是Oracle JDBC驱动内部使用的。我们无法直接修改Oracle驱动的代码。因此,我们需要寻找其他解决方案。 可能的解决方案: 1. 升级Oracle JDBC驱动:查看是否有新版本的驱动修复了这个问题。 2. 在应用程序中手动清除:如果我们知道这个ThreadLocal是在哪里设置的,可以在应用程序中适当的位置(比如在ServletContextListener的contextDestroyed方法中)清除它。 然而,由于警告中并没有明确说明是应用程序的哪个部分创建了这个ThreadLocal,所以我们需要进行一些调试以确定如何清除。 但是,从错误信息中,我们知道ThreadLocal的值类型是oracle.sql.TypeDescriptorFactory,并且键的类型是java.lang.ThreadLocal。这意味着我们可以尝试通过反射来清除这个ThreadLocal。 但是,请注意:使用反射来清除ThreadLocal有一定的风险,因为它依赖于内部实现,而且可能会影响其他应用程序(如果多个应用程序部署在同一个容器中)。 另一种方法是,我们可以尝试在应用程序关闭时,强制让加载Oracle驱动的类加载器被卸载。通常,我们可以在contextDestroyed方法中调用如下代码: ```java // 清除当前线程的ThreadLocals Thread.currentThread().setContextClassLoader(null); // 另外,我们也可以尝试清除所有线程?但这很难,因为无法访问所有线程。 // 更直接的方法是:尝试调用DriverManager.deregisterDriver()来注销Oracle驱动,这样可能促使驱动清理其资源。 ``` 然而,由于这个ThreadLocal是在Oracle驱动内部使用的,我们可能需要按照Oracle驱动的规范来清理。查阅Oracle驱动的文档,看看是否有提供清理资源的方法。 另外,Tomcat也提供了一些机制来防止内存泄漏,比如在Context元素中设置<Resources cachingAllowed="false"/>,但这可能不是针对这个问题的。 鉴于上述情况,我们可以尝试以下步骤: 步骤1:在应用程序关闭时,清除当前线程的ThreadLocal(但注意,这个ThreadLocal可能存在于多个线程中,而不仅仅是当前线程)。 步骤2:尝试通过反射来清除ThreadLocal。但是,由于ThreadLocal的键是未知的,我们需要遍历当前线程的ThreadLocalMap,找到值为oracle.sql.TypeDescriptorFactory的项,然后将其移除。 但是,注意:这个ThreadLocal可能是在系统类加载器加载的类中创建的,因此我们不能直接访问到它。不过,我们可以通过反射访问Thread类的threadLocals字段。 以下是一个示例代码,用于在ServletContextListener中清除当前线程和Tomcat线程的ThreadLocal: 注意:此代码需要谨慎使用,因为它依赖于JDK内部实现,并且可能在不同版本的JDK中有所不同。 示例代码: 我们创建一个ServletContextListener,在contextDestroyed方法中尝试清除ThreadLocal。 ```java import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Vector; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; @WebListener public class ThreadLocalCleanupListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { // 初始化操作 } @Override public void contextDestroyed(ServletContextEvent sce) { // 清除当前线程的ThreadLocals cleanupThreadLocals(Thread.currentThread()); // 尝试清除Tomcat的工作线程(这比较复杂,不一定能访问到所有线程) // 另外,我们也可以尝试清除Tomcat的线程池中的线程?但Tomcat没有提供标准方法。 // 注销JDBC驱动 deregisterDrivers(); } private void cleanupThreadLocals(Thread thread) { try { // 获取Thread类中的threadLocals字段 Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); threadLocalsField.setAccessible(true); Object threadLocalMap = threadLocalsField.get(thread); if (threadLocalMap != null) { // 获取ThreadLocalMap的Class Class<?> tlmClass = threadLocalMap.getClass(); // 获取ThreadLocalMap中的table字段 Field tableField = tlmClass.getDeclaredField("table"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(threadLocalMap); if (table != null) { // 遍历table数组,找到需要清理的Entry for (Object entry : table) { if (entry != null) { // 获取Entry的value字段 Field valueField = entry.getClass().getDeclaredField("value"); valueField.setAccessible(true); Object value = valueField.get(entry); // 如果value是oracle.sql.TypeDescriptorFactory,则清除这个Entry if (value != null && value.getClass().getName().equals("oracle.sql.TypeDescriptorFactory")) { // 将value设为null,这样Entry就可以被回收了 valueField.set(entry, null); // 另外,我们也可以将这个Entry的key设为null,这样在下次ThreadLocalMap调整时会清理 Field keyField = entry.getClass().getDeclaredField("referent"); keyField.setAccessible(true); keyField.set(entry, null); } } } } } } catch (NoSuchFieldException | IllegalAccessException e) { // 忽略异常,因为可能在不同版本的JDK中字段名不同或者无法访问 e.printStackTrace(); } } private void deregisterDrivers() { // 注销所有JDBC驱动 java.util.Enumeration<java.sql.Driver> drivers = java.sql.DriverManager.getDrivers(); while (drivers.hasMoreElements()) { java.sql.Driver driver = drivers.nextElement(); try { java.sql.DriverManager.deregisterDriver(driver); } catch (java.sql.SQLException e) { e.printStackTrace(); } } } } ``` 但是,请注意: 1. 这个方法只能清除当前线程的ThreadLocalMap。而Tomcat的线程池中的线程可能没有被清除。我们需要清除所有线程的ThreadLocalMap,但这几乎是不可能的,因为我们无法访问所有线程。 2. 反射操作可能会因为JDK版本变化而失效,或者由于安全管理器而失败。 3. 清除ThreadLocalMap中的Entry可能会影响其他应用程序(如果同一个Tomcat部署了多个应用),因为线程是Tomcat管理的,会被多个应用共享。所以,我们应该只清理我们关心的那个值(即oracle.sql.TypeDescriptorFactory)。 另外,Tomcat 8以后提供了更强大的内存泄漏防护机制,我们可以尝试配置Tomcat来避免这个问题,比如在context.xml中配置: ```xml <Context> ... <Loader loaderClass="org.apache.catalina.loader.ParallelWebappClassLoader" /> ... </Context> ``` 或者,我们可以尝试在停止应用时,强制让Tomcat更新线程(通过配置renewThreadsWhenStoppingContext属性)。但请注意,Tomcat 8.5.23版本以后,这个属性已经不再使用,因为Tomcat会自动更新线程。 实际上,Tomcat在停止上下文时会尝试更新线程,以避免内存泄漏。但是,如果线程更新失败(例如,线程正在执行长时间任务),那么内存泄漏仍然可能发生。 因此,最好的解决方案是: 1. 升级Oracle JDBC驱动到最新版,查看是否已经修复。 2. 如果问题仍然存在,尝试在应用停止时,通过上面的监听器清除当前线程的ThreadLocal(可能不够彻底)。 3. 考虑升级Tomcat到最新版本,因为新版本可能有更好的内存泄漏处理机制。 另外,我们还可以在应用停止时,调用以下方法(适用于Tomcat)来清理线程池中的线程的ThreadLocal: 但是,Tomcat没有提供直接访问线程池中所有线程的API。因此,我们可能需要通过反射获取线程池,然后获取所有工作线程,再分别清理。 这非常危险,且不推荐在生产环境中使用。因此,我们通常只清理当前线程(因为Tomcat在停止应用时会使用当前线程来发送停止事件)和可能的一些管理线程(如RMI线程)。 总结:虽然我们可以尝试一些方法,但最稳妥的还是等待驱动修复或者升级Tomcat版本。 由于问题复杂,且涉及第三方库,我们可能无法彻底解决。所以,如果这个警告只是偶尔出现,并且没有造成实际的内存泄漏(因为Tomcat会尝试更新线程),我们可以忽略它。 但是,如果出现内存泄漏,我们就需要采取上述措施。 注意:上面的示例代码仅供尝试,请根据实际情况调整。 下面是完整的ServletContextListener实现,包括注销驱动和清理当前线程的ThreadLocal(针对oracle.sql.TypeDescriptorFactory)。 另外,我们还可以尝试清理Tomcat的RMI线程(因为错误日志中出现了RMI TCP Connection线程)。但是,RMI线程是由Tomcat管理的,我们同样无法直接清理。 因此,我们只能做到: 1. 清理当前线程。 2. 注销驱动。 代码如下: ```java import java.lang.reflect.Field; import java.util.Enumeration; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; @WebListener public class OracleThreadLocalCleanupListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { // 初始化操作 } @Override public void contextDestroyed(ServletContextEvent sce) { // 清理当前线程的ThreadLocal cleanupThreadLocals(Thread.currentThread()); // 注销所有JDBC驱动,特别是Oracle驱动 deregisterDrivers(); } private void cleanupThreadLocals(Thread thread) { try { // 获取Thread类中的threadLocals字段 Field threadLocalsField = Thread.class.getDeclaredField("threadLocals"); threadLocalsField.setAccessible(true); Object threadLocalMap = threadLocalsField.get(thread); if (threadLocalMap != null) { // 获取ThreadLocalMap的Class Class<?> tlmClass = threadLocalMap.getClass(); // 获取ThreadLocalMap中的table字段 Field tableField = tlmClass.getDeclaredField("table"); tableField.setAccessible(true); Object[] table = (Object[]) tableField.get(threadLocalMap); if (table != null) { // 遍历table数组,找到需要清理的Entry for (Object entry : table) { if (entry != null) { // 获取Entry的value字段 Field valueField = entry.getClass().getDeclaredField("value"); valueField.setAccessible(true); Object value = valueField.get(entry); // 如果value是oracle.sql.TypeDescriptorFactory,则清除这个Entry if (value != null && "oracle.sql.TypeDescriptorFactory".equals(value.getClass().getName())) { // 将value设为null valueField.set(entry, null); // 将keyThreadLocal的引用)设为null,这样Entry就可以被回收了 Field keyField = entry.getClass().getDeclaredField("referent"); keyField.setAccessible(true); keyField.set(entry, null); } } } } } } catch (Exception e) { // 忽略异常 e.printStackTrace(); } } private void deregisterDrivers() { // 注销所有JDBC驱动 Enumeration<Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); try { DriverManager.deregisterDriver(driver); System.out.println("Deregistering JDBC driver: " + driver); } catch (SQLException e) { System.out.println("Error deregistering JDBC driver: " + driver); e.printStackTrace(); } } } } ``` 请注意,这段代码可能无法完全解决问题,因为可能有多个线程(如Tomcat线程池中的线程)持有这个ThreadLocal。但是,它可以帮助减少内存泄漏的可能性。 最后,建议升级Oracle驱动和Tomcat,并监控内存使用情况。 如果问题仍然存在,可能需要更深入地研究Oracle驱动和Tomcat的类加载机制。 相关问题: 由于我们的解决方案涉及到多方面的知识,包括ThreadLocalTomcat类加载机制、内存泄漏等,下面提出几个相关问题。 </think>### 分析及解决方案 该错误是 Tomcat 应用停止时未正确清理 ThreadLocal 导致的内存泄漏警告。核心问题在于应用创建的 ThreadLocal 存储了 Oracle JDBC 驱动相关对象(`oracle.sql.TypeDescriptorFactory`),应用停止时未移除,导致类加载器无法被 GC 回收。 #### 解决方案步骤: 1. **定位 ThreadLocal 使用位置** 在代码中搜索 `ThreadLocal` 相关声明,特别是涉及 Oracle 数据库操作的部分(如连接池、类型转换等)。 2. **手动清理 ThreadLocal** 最佳实践是在应用关闭时(ServletContextListener)或使用后主动移除 ThreadLocal 数据。 3. **使用 JDBC 连接池的清理机制** 如果使用连接池(如 HikariCP、Tomcat JDBC),配置连接池的清理钩子。 #### 修复代码示例(ServletContextListener) ```java import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; import java.sql.Driver; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Enumeration; @WebListener public class ThreadLocalCleanupListener implements ServletContextListener { // 声明与错误日志中相同的ThreadLocal变量(需替换为实际类) private static final ThreadLocal<Class<?>> oracleTypeLocal = new ThreadLocal<>(); @Override public void contextDestroyed(ServletContextEvent sce) { // 1. 清理自定义ThreadLocal oracleTypeLocal.remove(); // 显式移除 // 2. 清理所有线程中的ThreadLocal(扩展方案) cleanAllThreadLocals(); // 3. 注销JDBC驱动(防止驱动持有引用) deregisterJdbcDrivers(); } private void cleanAllThreadLocals() { try { // 反射清理Thread.threadLocals字段 Thread[] threads = getActiveThreads(); for (Thread thread : threads) { clearThreadLocals(thread); } } catch (Exception e) { System.err.println("ThreadLocal清理失败: " + e.getMessage()); } } private Thread[] getActiveThreads() { ThreadGroup group = Thread.currentThread().getThreadGroup(); while (group.getParent() != null) { group = group.getParent(); } Thread[] threads = new Thread[group.activeCount()]; group.enumerate(threads); return threads; } private void clearThreadLocals(Thread thread) throws Exception { Field threadLocals = Thread.class.getDeclaredField("threadLocals"); threadLocals.setAccessible(true); threadLocals.set(thread, null); // 清空ThreadLocalMap } private void deregisterJdbcDrivers() { Enumeration<Driver> drivers = DriverManager.getDrivers(); while (drivers.hasMoreElements()) { Driver driver = drivers.nextElement(); try { DriverManager.deregisterDriver(driver); System.out.println("注销JDBC驱动: " + driver.getClass().getName()); } catch (SQLException e) { System.err.println("注销驱动失败: " + driver.getClass().getName()); } } } @Override public void contextInitialized(ServletContextEvent sce) { // 初始化逻辑(可选) } } ``` #### 关键代码解释: 1. **`oracleTypeLocal.remove()`** 直接清除应用中定义的 ThreadLocal 数据(需替换为实际变量名)。 2. **反射清理 `threadLocals`** - 通过反射获取 JVM 中所有活跃线程 - 将每个线程的 `threadLocals` 字段设为 `null`,彻底释放内存 - *注意:此操作需 JDK 权限,生产环境需测试兼容性* 3. **注销 JDBC 驱动** - 防止驱动持有静态引用导致类加载器泄漏 - 适用于 Oracle 等会注册 `DriverManager` 的驱动 #### 预防措施: 1. **规范 ThreadLocal 使用** 遵循 try-finally 模式确保清理: ```java try { oracleTypeLocal.set(someValue); // ...业务代码 } finally { oracleTypeLocal.remove(); // 必须清理 } ``` 2. **升级组件** - 将 Oracle JDBC 驱动升级至最新版(可能已修复内部 ThreadLocal 问题) - 使用 Tomcat 9.0.50+ 版本(优化了类加载器泄漏检测) 3. **连接池配置**(以 HikariCP 为例) ```properties # application.properties spring.datasource.hikari.leak-detection-threshold=60000 # 启用泄漏检测 spring.datasource.hikari.max-lifetime=1800000 # 30分钟连接回收 ``` --- ### 根本原因图解 ```mermaid graph LR A[Web应用停止] --> B[ThreadLocal未清除] B --> C[oracle.sql.TypeDescriptorFactory 未被回收] C --> D[应用类加载器被强引用] D --> E[类加载器无法GC] E --> F[PermGen/Metaspace 内存泄漏] ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值