ThreadLocal & Memory Leak

在多个帖子中都提到了它:不正确使用导致内存泄漏。我正在努力理解内存泄漏是如何发生的。ThreadLocalThreadLocal

我唯一想到的情况如下:

Web服务器维护一个线程池(例如,对于servlet)。如果未删除 中的变量,则这些线程可能会造成内存泄漏,因为线程不会失效。ThreadLocal

此方案没有提到“烫发空间”内存泄漏。这是内存泄漏的唯一(主要)用例吗?


答案 1

PermGen耗尽类加载器泄漏相结合通常是由类加载器泄漏引起的。

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

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

如果 Web 应用程序未能清除关机时的引用,则会发生不好的事情:
因为工作线程通常永远不会死亡,并且对 的引用是静态的,因此该值仍会引用 的实例 ,Web 应用程序的类 - 即使 Web 应用程序已停止!

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

=> 这是烫发泄漏 这种泄漏

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


答案 2

这个问题的公认答案,以及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日志上的幻像内存泄漏。


推荐