ThreadLocal 中的内存泄漏
Contents
缘起
发现线上项目, 在 Tomcat shutdown 时会报告可能的内存泄漏问题
日志如下
Sep 20, 2016 12:41:33 PM org.apache.catalina.loader.WebappClassLoader clearReferencesJdbc
SEVERE: The web application [] registered the JDBC driver [com.mysql.jdbc.Driver] but failed to unregister it when the web application was stopped. To prevent a memory leak, the JDBC Driver has been forcibly unregistered.
Sep 20, 2016 12:41:33 PM org.apache.catalina.loader.WebappClassLoader clearReferencesThreads
SEVERE: The web application [] appears to have started a thread named [Timer-0] but has failed to stop it. This is very likely to create a memory leak.
Sep 20, 2016 12:41:33 PM org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks
SEVERE: The web application [] created a ThreadLocal with key of type [com.company.util.DateUtil$4] (value [com.company.util.DateUtil$4@2831fe1d]) and a value of type [java.text.SimpleDateFormat] (value [java.text.SimpleDateFormat@f67a0200]) but failed to remove it when the web application was stopped. Threads are going to be renewed over time to try and avoid a probable memory leak.
Sep 20, 2016 12:41:33 PM org.apache.catalina.loader.WebappClassLoader checkThreadLocalMapForLeaks
这是同事由于自己写了个 DateUtil 工具类用 ThreadLocal 来封装 SimpleDateFormat (它是线程不安全的).
它的代码如下
public class DateUtil {
private DateUtil() {
}
public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss";
public static final ThreadLocal<SimpleDateFormat> df_seconds = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat(YYYY_MM_DD_HH_MM_SS);
}
};
...
}
原因
Google 了下资料, 发现 stackoverflow 上有个比较好的解释, 我这里顺便翻译一下过来
与 ThreadLocal 组合的 PermGen 耗竭通常是由于 classloader 泄漏导致的. 例如 假设一个应用服务器有一个 worker 线程池. 它们会一直 keep alive 直到应用服务器终止. 一个部署好的 web 应用在它的类中为了保存一些
thread-local的数据而使用一个static ThreadLocal, 数据是 web 应用的其他类的实例(让我们叫它SomeClass). 这是在 worker 线程内完成的(例如源于一个 HTTP 请求动作)重要: 通过定义, 一个引用到一个
ThreadLocal value是会一直保持的, 直到所属线程死亡或ThreadLocal自身是不再可达.如果一个 web 应用在 shutdown 的时候不能清除这些引用, 一些糟糕的事将会发生: 由于 worker 线程通常不会死亡并且引用的 ThreadLocal 是
static,ThreadLocal value会仍然引用了SomeClass的实例, 一个 web 应用的 class, 尽管 web 应用已经停止了!结果就是, web 应用的 classloader 不能被GC, 这意味着所有web 应用的 class(以及所有的
static数据)仍然是已加载的(remain loaded)(这会影响到 PermGen 内存池以及堆). 每次迭代重新部署 web 应用将会不断增加 PermGen(以及堆) 的使用.这就是 PermGen 泄漏.
这种典型的一个例子就是 log4j 的 bug (在此期间已经被 fixed).
下面的讨论也很值得看一下.
问题是 - 为什么 web server 在应用已经 stop 的情况下, 没尝试 kill worker thread ? 我猜: web server 为会特定的应用创建 worker thread. 如果 web server 没 kill 这些 threads, 甚至在没有 ThreadLocal 的情况下, 这些 threads 将仍然在这里.
threads 永不应该被 kill, 它们仅应该被 notified/interrupted (通知/中断) 而通常是由它们自己来停止的. 并且, threads 的创建是比较昂贵的, 并且在同一个 container 内常常是跨应用共享的 – 但这是特定于具体实现的. 然而, 有些应用服务器会删除所有所属的 web 应用的线程(取决于 产品/配置), 或者定期刷新其线程以防止这种泄漏. 这也是基于特定实现的. 参看 Tomcat 的细节
感谢, 非常好的解释. 清楚了为什么 ThreadLocal 变量不会被 GC, 但为什么 classloader 以及其他的 class 也不会被 GC ? 其他的 class 不必带有 ThreadLocal 变量来处理逻辑的.
问题在于 ThreadLocal 的 value 也不会被 GC. 由于 value 是一个 shutdown 的 web 应用的一个类实例, 因此它的 classloader 以及其他的 class 也都不会被 GC 掉.
完美的解释!
解决
SimpleDateFormat 的创建是比较昂贵的, 所以要避免频繁创建它.
- 使用其他的线程安全的日期时间库(如 Joda-Time)(例如
DateTimeFormatter它是线程安全的, 并且是不可变对象) - 用完 ThreadLocal 后, 要直接删除 (调用ThreadLocal 的 remove() 方法)
JDBC
可以看到, 上面还有个 JDBC 的问题. 这个解决如下(尽管Tomcat 自动帮我们卸载 JDBC Driver了), 我们写个 ServletContextListener 来手动卸载进行清理
在 web.xml 注册监听器
<listener>
<listener-class>com.company.listener.ApplicationCleanupListener</listener-class>
</listener>
java代码
@Override
public void contextDestroyed(ServletContextEvent event) {
// 手动卸载 JDBC 驱动.
Enumeration<Driver> drivers = DriverManager.getDrivers();
while (drivers.hasMoreElements()) {
Driver driver = drivers.nextElement();
try {
DriverManager.deregisterDriver(driver);
log.info("deregistering jdbc driver: {}", driver);
} catch (SQLException e) {
log.error("deregistering jdbc driver: {} {}", driver, e);
}
}
}