ThreadLocal & Memory Leak
在多个帖子中都提到了它:不正确使用导致内存泄漏。我正在努力理解内存泄漏是如何发生的。ThreadLocal
ThreadLocal
我唯一想到的情况如下:
Web服务器维护一个线程池(例如,对于servlet)。如果未删除 中的变量,则这些线程可能会造成内存泄漏,因为线程不会失效。
ThreadLocal
此方案没有提到“烫发空间”内存泄漏。这是内存泄漏的唯一(主要)用例吗?
在多个帖子中都提到了它:不正确使用导致内存泄漏。我正在努力理解内存泄漏是如何发生的。ThreadLocal
ThreadLocal
我唯一想到的情况如下:
Web服务器维护一个线程池(例如,对于servlet)。如果未删除 中的变量,则这些线程可能会造成内存泄漏,因为线程不会失效。
ThreadLocal
此方案没有提到“烫发空间”内存泄漏。这是内存泄漏的唯一(主要)用例吗?
PermGen耗尽与类加载器泄漏相结合通常是由类加载器泄漏引起的。
例如:
假设一个应用程序服务器具有工作线程池。
它们将保持活动状态,直到应用程序服务器终止。
已部署的 Web 应用程序在其一个类中使用静态,以便存储一些线程本地数据,这是 Web 应用程序的另一个类(我们称之为 )的实例。这是在工作线程内完成的(例如,此操作源自 HTTP 请求)。
重要提示:
根据定义,对值的引用将一直保留,直到“拥有”线程死亡或 ThreadLocal 本身不再可访问。
如果 Web 应用程序未能清除对关机时的引用,则会发生不好的事情:
因为工作线程通常永远不会死亡,并且对 的引用是静态的,因此该值仍会引用 的实例 ,Web 应用程序的类 - 即使 Web 应用程序已停止!
因此,Web 应用程序的类装入器无法进行垃圾回收,这意味着 Web 应用程序的所有类(和所有静态数据)都保持加载状态(这会影响 PermGen 内存池以及堆)。
Web 应用程序的每次重新部署迭代都会增加 permgen(和堆)的使用率。
=> 这是烫发泄漏 这种泄漏
的一个流行例子是log4j中的这个错误(同时修复)。ThreadLocal
ThreadLocal
SomeClass
ThreadLocal
ThreadLocal
ThreadLocal
ThreadLocal
SomeClass
这个问题的公认答案,以及Tomcat关于这个问题的“严重”日志具有误导性。关键引用是:
根据定义,对 ThreadLocal 值的引用将一直保留到“拥有”线程死亡或 ThreadLocal 本身不再可访问。
在这种情况下,对 ThreadLocal 的唯一引用位于现在已成为 GC 目标的类的静态 final 字段中,以及来自工作线程的引用中。但是,从工作线程到 ThreadLocal 的引用是弱引用!
但是,ThreadLocal 的值不是弱引用。因此,如果 ThreadLocal 的值中有对应用程序类的引用,则这些引用将保留对 ClassLoader 的引用并阻止 GC。但是,如果您的 ThreadLocal 值只是整数或字符串或其他一些基本对象类型(例如,上述内容的标准集合),那么应该没有问题(它们只会阻止引导/系统类装入器的 GC,这无论如何都不会发生)。
当你完成一个 ThreadLocal 时,显式清理它仍然是一个很好的做法,但是在引用的 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 Main ClassLoader: 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 Main ClassLoader: CustomClassLoader@e76cbf7 *** CustomClassLoader finalized!
(Hashtable也是如此)。因此,在log4j的情况下,他们没有内存泄漏或任何类型的错误。他们已经清除了 Hashtable,这足以确保类加载器的 GC。IMO,该错误位于Tomcat中,它不加选择地在关闭时为所有尚未显式.remove()d的ThreadLocal记录这些“严重”错误,无论它们是否具有对应用程序类的强引用。似乎至少有一些开发人员正在投入时间和精力来“修复”草率的Tomcat日志上的幻像内存泄漏。