这个问题的公认答案以及来自 Tomcat 关于这个问题的“严重”日志具有误导性。关键引用是:
根据定义,对 ThreadLocal 值的引用会一直保留到“拥有”线程死亡或者如果 ThreadLocal 本身不再可访问。 [我的重点]。
在这种情况下,对 ThreadLocal 的唯一引用是在现在已成为 GC 目标的类的静态 final 字段中,以及来自工作线程的引用。但是,工作线程对 ThreadLocal 的引用是 WeakReferences!
但是,ThreadLocal 的值不是弱引用。因此,如果您在 ThreadLocal 的 values 中有对应用程序类的引用,那么这些将保持对 ClassLoader 的引用并防止 GC。但是,如果您的 ThreadLocal 值只是整数或字符串或其他一些基本对象类型(例如,上述的标准集合),那么应该没有问题(它们只会阻止引导/系统类加载器的 GC,即无论如何都不会发生)。
当你完成一个 ThreadLocal 时,显式清理它仍然是一个好习惯,但是在 the cited log4j bug 的情况下,天肯定没有塌下来(从报告中可以看出,该值是一个空的 Hashtable )。
这里有一些代码来演示。首先,我们创建一个没有父级的基本自定义类加载器实现,在最终确定时打印到 System.out:
import java.net.*;
public class CustomClassLoader extends URLClassLoader {
public CustomClassLoader(URL... urls) {
super(urls, null);
}
@Override
protected void finalize() {
System.out.println("*** CustomClassLoader finalized!");
}
}
然后我们定义一个驱动程序应用程序,它创建这个类加载器的一个新实例,使用它来加载一个具有 ThreadLocal 的类,然后删除对类加载器的引用,允许它被 GC'ed。首先,在 ThreadLocal 值是对自定义类加载器加载的类的引用的情况下:
import java.net.*;
public class Main {
public static void main(String...args) throws Exception {
loadFoo();
while (true) {
System.gc();
Thread.sleep(1000);
}
}
private static void loadFoo() throws Exception {
CustomClassLoader cl = new CustomClassLoader(new URL("file:/tmp/"));
Class<?> clazz = cl.loadClass("Main$Foo");
clazz.newInstance();
cl = null;
}
public static class Foo {
private static final ThreadLocal<Foo> tl = new ThreadLocal<Foo>();
public Foo() {
tl.set(this);
System.out.println("ClassLoader: " + this.getClass().getClassLoader());
}
}
}
当我们运行它时,我们可以看到 CustomClassLoader 确实没有被垃圾回收(因为主线程中的本地线程引用了由我们的自定义类加载器加载的 Foo 实例):
$java 主要
类加载器:CustomClassLoader@7a6d084b
但是,当我们将 ThreadLocal 更改为包含对简单 Integer 而不是 Foo 实例的引用时:
public static class Foo {
private static final ThreadLocal<Integer> tl = new ThreadLocal<Integer>();
public Foo() {
tl.set(42);
System.out.println("ClassLoader: " + this.getClass().getClassLoader());
}
}
然后我们看到自定义类加载器被垃圾回收了(因为主线程上的本地线程只有对系统类加载器加载的整数的引用):
$java 主要
类加载器:CustomClassLoader@e76cbf7
*** CustomClassLoader 完成!
(Hashtable 也是如此)。所以在 log4j 的情况下,它们没有内存泄漏或任何类型的错误。他们已经在清除 Hashtable,这足以确保类加载器的 GC。 IMO,该错误存在于 Tomcat 中,它在关闭时不加选择地为所有未明确 .remove()d 的 ThreadLocals 记录这些“严重”错误,无论它们是否对应用程序类具有强引用。似乎至少有一些开发人员正在投入时间和精力来“修复”草率 Tomcat 日志的幻象内存泄漏。