线程本地和内存泄漏

在多个post中提到:不恰当的使用ThreadLocal导致内存泄漏。 我正在努力了解如何使用ThreadLocal发生内存泄漏。

我唯一的想法是如下:

一个Web服务器维护一个线程池(例如servlet)。 如果ThreadLocal中的variables没有被删除,这些线程可能会造成内存泄漏,因为线程不会死亡。

这种情况下没有提到“Perm Space”内存泄漏。 这是内存泄漏的唯一(主要)用例吗?

ThreadLocal组合的PermGen穷举通常是由类加载器泄漏引起的。

一个例子:
想象一下,有一个工作线程池的应用程序服务器。
他们将保持活着,直到应用程序服务器终止。
已部署的Web应用程序在其一个类中使用了一个静态 ThreadLocal ,以便存储一些线程本地数据,即Web应用程序的另一个类的实例(可以称为SomeClass )。 这是在工作线程内完成的(例如,这个动作来自HTTP请求 )。

重要:
根据定义 ,对ThreadLocal 的引用会一直保留,直到“拥有”线程死亡或者ThreadLocal本身不再可用。

如果Web应用程序在closures时 未能清除对 ThreadLocal 的引用 ,则会发生不良情况:
因为工作者线程通常不会死亡,并且对ThreadLocal的引用是静态的,所以即使Web应用程序已经停止ThreadLocal仍然引用 SomeClass (一个Web应用程序的类)的实例

因此,Web应用程序的类加载器无法进行垃圾回收 ,这意味着Web应用程序的所有类 (以及所有静态数据)都将保持加载 (这将影响PermGen内存池以及堆)。
Web应用程序的每个重新部署迭代都会增加permgen(和堆)的使用。

=>这是permgen泄漏

这种泄漏的一个stream行的例子是log4j中的这个bug (同时被修复)。

这个问题被接受的答案,以及Tomcat关于这个问题的“严重”日志都是误导性的。 重要的报价是:

根据定义,对ThreadLocal值的引用会一直保留,直到“拥有”线程死亡或者ThreadLocal本身不再可用。 [我的重点]。

在这种情况下,唯一对ThreadLocal的引用位于现在已成为GC目标的类的静态最终字段中,以及来自工作线程的引用。 但是,从Worker线程到ThreadLocal的引用是WeakReferences !

但是,ThreadLocal的值不是弱引用。 因此,如果您在应用程序类的ThreadLocal的中有引用,那么这些将保持对ClassLoader的引用并阻止GC。 但是,如果您的ThreadLocal值只是整数或string或其他基本对象types(例如,上述标准集合),那么应该没有问题(它们只会阻止启动/系统类加载器的GC,永远不会发生)。

当你完成这个工作时,明确清理一个ThreadLocal仍然是一个好习惯,但是在被引用的log4j错误的情况下,天空绝对不会下降(正如你从报告中看到的,值是一个空的Hashtable)。

这里有一些代码来演示。 首先,我们创build一个基本的自定义类加载器实现,没有父对象,可以在完成时打印到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!"); } } 

然后我们定义一个驱动程序应用程序,它创build这个类加载器的一个新实例,使用它来加载一个带有ThreadLocal的类,然后删除对类加载器的引用,从而允许它被GC化。 首先,在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定稿!

(Hashtable也是如此)。 所以在log4j的情况下,他们没有内存泄漏或任何types的错误。 他们已经清除了Hashtable,这足以确保类加载器的GC。 国际海事组织,错误是在Tomcat的,不分青红皂白地logging所有的ThreadLocals没有明确.remove()d,closures这些“严重”的错误,无论他们是否持有一个应用程序类的强引用。 似乎至less有一些开发人员正投入时间和精力来修复那些马虎的Tomcat日志中的幻影内存泄漏。

线程本地没有什么本质的错误:它们不会导致内存泄漏。 他们不慢。 他们比非线程本地更对本地(即他们有更好的信息隐藏属性)。 当然,它们可能会被滥用,但大多数其他编程工具也是如此。

请参阅Joshua Bloch的这个链接

以前的post解释了这个问题,但不提供任何解决scheme。 我发现没有办法“清除”ThreadLocal。 在我处理请求的容器环境中,我最后在每个请求的末尾调用了.remove()。 我意识到,使用容器pipe理的事务可能会有问题。

在下面的代码中,迭代中的实例t不能是GC。 这可能是ThreadLocal & Memory Leak一个例子

 public class MemoryLeak { public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 100000; i++) { TestClass t = new TestClass(i); t.printId(); t = null; } } }).start(); } static class TestClass{ private int id; private int[] arr; private ThreadLocal<TestClass> threadLocal; TestClass(int id){ this.id = id; arr = new int[1000000]; threadLocal = new ThreadLocal<>(); threadLocal.set(this); } public void printId(){ System.out.println(threadLocal.get().id); } } } 

这是ThreadLocal的替代方法,它没有内存泄漏问题:

 class BetterThreadLocal<A> { Map<Thread, A> map = Collections.synchronizedMap(new WeakHashMap()); A get() { ret map.get(Thread.currentThread()); } void set(A a) { if (a == null) map.remove(Thread.currentThread()); else map.put(Thread.currentThread(), a); } } 

注意:有一个新的内存泄漏情况,但这是不太可能的,可以通过遵循一个简单的指导线来避免。 该场景保持对BetterThreadLocal中的Thread对象的强引用。

我从来没有保持对线程的强引用,因为你总是想让线程在工作完成时被GC'd …所以你去了:一个内存泄漏的ThreadLocal。

有人应该以此为基准。 我期望它跟Java的ThreadLocal一样快(实质上做一个弱散列映射查找,只有一个查找线程,另一个是ThreadLocal)。

JavaX中的示例程序。

最后一点:我的系统( JavaX )也跟踪所有的WeakHashMaps并定期清理它们,所以最后一个超级不可能的漏洞被插入(长期WeakHashMaps从不查询,但仍然有陈旧的条目)。