ThreadLocal和内存泄漏

San*_*dal 49 java multithreading memory-leaks permgen thread-local

在多个帖子中提到:不当使用ThreadLocal原因内存泄漏.我正在努力了解内存泄漏将如何发生ThreadLocal.

我发现它的唯一情况如下:

Web服务器维护一个线程池(例如,用于servlet).如果ThreadLocal没有删除变量,那些线程可以创建内存泄漏,因为线程没有死亡.

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

MRa*_*ser 71

与组合使用的PermGen耗尽ThreadLocal通常是由类加载器泄漏引起的.

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

重要:
根据定义,在"拥有"线程死亡或者ThreadLocal本身不再可访问之前,将保留对ThreadLocal 的引用.

如果Web应用程序无法清除参考ThreadLocal 上关机,不好的事情会发生:
由于工作线程通常不会死和参考ThreadLocal是静态的,该ThreadLocal仍然引用的情况下SomeClass,Web应用程序的类- 即使Web应用程序已停止!

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

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

一个流行的例子是 log4j中的这个错误(同时修复).

  • (可能是愚蠢的)问题是 - 为什么Web服务器在应用程序停止时不会尝试杀死该应用程序的工作线程?我猜/假设Web Server将创建特定于应用程序的工作线程.如果webserver没有杀死那些线程,即使没有ThreadLocal,线程也会保留在那里. (3认同)
  • 永远不要杀死线程,仅应通知/中断线程以使其自行轻轻终止。此外,创建线程的成本很高,并且通常在同一容器内的多个应用程序之间共享线程-但这是特定于实现的。但是,某些应用程序服务器会丢弃已停止的Web应用程序的所有线程(取决于产品/配置),或定期更新其线程,以防止此类泄漏。这也是特定于实现的。看一下tomcat的详细信息:http://tomcat.apache.org/tomcat-7.0-doc/config/executor.html (2认同)
  • MRalwasser,完美解释! (2认同)

Nei*_*den 30

这个问题的公认答案以及Tomcat关于这个问题的"严重"日志都是误导性的.关键的话是:

根据定义,保持对ThreadLocal值的引用,直到"拥有"线程终止或者ThreadLocal本身不再可访问为止.[我的重点].

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

但是,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!");
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我们定义一个驱动程序应用程序,它创建这个类加载器的新实例,使用它来加载一个带有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());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

当我们运行它时,我们可以看到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());
    }
}
Run Code Online (Sandbox Code Playgroud)

然后我们看到自定义类加载器现在垃圾收集的(因为主线程上的本地线程只引用了系统类加载器加载的整数):

$ java Main
ClassLoader: CustomClassLoader@e76cbf7
*** CustomClassLoader finalized!

(Hashtable也是如此).所以在log4j的情况下,他们没有内存泄漏或任何类型的错误.他们已经清除了Hashtable,这足以确保类加载器的GC.IMO,这个bug出现在Tomcat中,它会在关闭所有未明确的.stove()d的ThreadLocals时不加选择地记录这些"SEVERE"错误,无论它们是否拥有对应用程序类的强引用.似乎至少有一些开发人员正在投入时间和精力来"修复"模糊内存泄漏的说法 - 如果马虎的Tomcat日志.

  • 有时,人们在 threadLocal 值中使用自定义类,甚至没有意识到这一点。例如,当您对线程本地值使用双括号初始化时,可能会发生这种情况,因为双括号初始化将创建一个匿名类。我为线程本地创建了一个修复程序,可以修复您的 Web 应用程序的类加载器泄漏,但保留对线程本地值的非阻塞访问:github.com/codesinthedark/ImprovedThreadLocal 现在您可以在 ThreadLocal 中使用您的类,并且您将没有内存webapp 重新部署时发生泄漏 (2认同)
  • @jackycflau JVM 加载的每个类都有一个对加载它的 ClassLoader 的引用 - `obj.getClass().getClassLoader()`。默认情况下,每个 ClassLoader 都有一个对它加载的每个类的引用。因此,如果您将自定义类存储在由自定义 ClassLoader 加载的 ThreadLocal 中,那么它将阻止 ClassLoader 被垃圾收集以及它加载的所有类。但是来自标准库的类是由引导类加载器加载的,无论如何它永远不会被垃圾收集。 (2认同)