2013-07-31 32 views
40

它在多个帖子中提到:不当使用ThreadLocal会导致内存泄漏。我努力了解如何使用ThreadLocal发生内存泄漏。ThreadLocal&内存泄漏

我已经想通了,唯一的方案是如下:

一个web服务器维护的线程池(如Servlet的)。如果因为线程不死而删除ThreadLocal中的变量,那些线程可能会造成内存泄漏。

此方案没有提及“Perm Space”内存泄漏。这是内存泄漏的唯一(主要)用例吗?

+0

你能澄清一下这个问题吗?你只想知道ThreadLocal是否会导致PermGen用尽? – MRalwasser

+0

是的。我想知道 - 1. ThreadLocal如何导致PermGen耗尽。 2.任何其他常见的错误使用ThreadLocal导致内存泄漏的情况。 –

+1

有详细的解释http://java.jiderhamn.se/2012/01/29/classloader-leaks-iv-threadlocal-dangers-and-why-threadglobal-may-have-been--更适当的-name /作者还提出了一个有趣的防漏库。 – Pino

回答

58

PermGen的exhaustionsThreadLocal经常被类加载器泄漏引起的。

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

重要:
By definition,到ThreadLocal基准一直保持,直到“拥有”线程死亡或者如果ThreadLocal的本身不再可达。

如果Web应用程序未能清除参考就关机的ThreadLocal,不好的事情会发生:
因为工作线程通常不会死和参考ThreadLocal是静态的,ThreadLocal仍然引用SomeClass的实例,即使Web应用程序已停止,也是一个Web应用程序的类 -

因此,Web应用程序的类加载器不能被垃圾收集,这意味着所有的Web应用程序的类(以及所有静态数据)保持加载(这会影响PermGen的内存池还有堆)。
Web应用程序的每次重新部署迭代都会增加permgen(和堆)使用率。

=>这是PermGen的泄漏这种泄漏的

一种流行的例子是在log4j的this bug(固定在同时)。

+2

(可能是愚蠢的)问题是 - 为什么Web Server在应用程序停止时不会尝试为该应用程序终止工作线程?我猜想/假设Web服务器将创建特定于应用程序的工作线程。如果web服务器不杀死这些线程,即使没有ThreadLocal,线程也会保留在那里。 –

+1

线程永远不会被杀死,它们只应该被通知/中断,以便自己轻轻地终止。此外,创建线程并且通常在同一个容器内的多个应用程序之间共享的代价很高 - 但这是针对实现的。但是,某些应用程序服务器会删除停止的Web应用程序的所有线程(取决于产品/配置),或者定期更新其线程以防止此类泄漏。这也是实现特定的。看看tomcat的详细信息:http://tomcat.apache.org/tomcat-7.0-doc/config/executor.html – MRalwasser

+0

@MRalwasser:Thnaks很好的解释。很明显,为什么踩踏本地可以不被垃圾收集,但为什么类加载者和其他类也不被垃圾收集?其他类不必与具有threadlocal变量的类做什么 – Victor

1

线程本地没有任何内在错误:它们不会导致内存泄漏。他们不慢。它们比非线程本地对手更本地化(即它们具有更好的信息隐藏属性)。它们可以被误用,当然,但这样可以大多数其他编程工具......

组合请参阅本link由Joshua布洛赫

+0

我在第一条语句中提到 - 如果ThreadLocal未正确使用.. –

0

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

23

对这个问题的接受答案,以及来自Tomcat的关于此问题的“严重”日志都具有误导性。关键的报价有:

根据定义,以一个ThreadLocal值的引用一直保持,直到“拥有”线程死亡或者如果ThreadLocal的本身不再可达。 [我的重点]。

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

但是,ThreadLocal的值不是弱引用。因此,如果您在应用程序类的ThreadLocal的中有引用,那么它们将保持对ClassLoader的引用并阻止GC。但是,如果您的ThreadLocal值只是整数或字符串或其他基本对象类型(例如,上述标准集合),那么应该没有问题(它们只会阻止引导/系统类加载器的GC,即反正也不会发生)。

它仍然是很好的做法,明确清理ThreadLocal的,当你用它做,但在天空中肯定是不属于的the cited log4j bug情况下(可以从报告中看到,该值是一个空哈希表) 。

下面是一些代码来演示。首先,我们创建一个没有父打印到的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化。首先,在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: [email protected] 

然而,当我们改变ThreadLocal的,而不是包含一个简单的整数,而不是一个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: [email protected] 
*** CustomClassLoader finalized! 

(同样是真正的Hashtable)。所以在log4j的情况下,他们没有内存泄漏或任何类型的错误。他们已经清除了Hashtable,这足以确保类加载器的GC。国际海事组织,这个错误是在Tomcat中,它不加区分地记录这些关于所有未明确使用.remove()d的ThreadLocals的“严重”错误,无论它们是否持有对应用程序类的强引用。看起来,至少有些开发人员正投入时间和精力来修复Tomcat日志猖獗的说法中的幻影内存泄漏。

+0

如何清理它? – Dejell

+1

有时候人们甚至在没有意识到的情况下使用threadLocal值的自定义类。例如,当您为线程本地值使用双大括号初始化时会发生这种情况,因为双大括号初始化将创建一个匿名类。 我为线程本地创建了一个修复程序,它修复了您的Web应用程序的类加载器泄漏,但保留了对线程本地值的非阻塞访问:github.com/codesinthedark/ImprovedThreadLocal 现在,您可以在ThreadLocal中使用您的类,在webapp重新部署时有内存泄漏 – user1944408

+0

这很有帮助 - 所以我们不应该为这个用例编写代码,或者有办法阻止这种内存泄漏。 – jtkSource

0

下面的代码中,迭代中的实例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); 
     } 
    } 
} 
0

一个例子这里是一个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)。

Sample program in JavaX.

而且最后要注意的:我的系统(JavaX)还跟踪所有WeakHashMaps,并定期清理它们,所以最后的晚餐,不可能孔插入那些从来查询(长寿命WeakHashMaps,但仍然有陈旧的条目)。