60

在多个帖子中提到:使用不当ThreadLocal导致内存泄漏。我正在努力理解使用ThreadLocal.

我想出的唯一方案如下:

Web 服务器维护一个线程池(例如,用于 servlet)。ThreadLocal如果由于线程没有死而未删除变量,则这些线程可能会造成内存泄漏。

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

4

7 回答 7

88

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

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

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

如果 Web 应用程序未能清除对 on shutdown 的引用ThreadLocal 就会发生不好的事情:
因为工作线程通常永远不会死亡并且对 的引用ThreadLocal是静态的,所以该ThreadLocal仍然引用SomeClassWeb 应用程序的类的实例——即使Web 应用程序已停止!

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

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

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

于 2013-07-31T15:59:44.120 回答
37

这个问题的公认答案以及 Tomcat 关于这个问题的“严重”日志具有误导性。那里的关键报价是:

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

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

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

完成后显式清理 ThreadLocal 仍然是一个好习惯,但在引用的 log4j 错误的情况下,天肯定没有塌下来(从报告中可以看出,该值是一个空的 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 主要
类加载器: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 主要
类加载器:CustomClassLoader@e76cbf7
*** CustomClassLoader 完成!

(哈希表也是如此)。因此,在 log4j 的情况下,它们没有内存泄漏或任何类型的错误。他们已经在清除 Hashtable,这足以确保类加载器的 GC。IMO,该错误存在于 Tomcat 中,它在关闭时不加选择地为所有未明确 .remove()d 的 ThreadLocals 记录这些“严重”错误,无论它们是否对应用程序类具有强引用。似乎至少有一些开发人员正在投入时间和精力来“修复”草率 Tomcat 日志的幻象内存泄漏。

于 2014-07-21T09:44:33.880 回答
3

线程局部变量本质上没有任何问题:它们不会导致内存泄漏。他们并不慢。它们比非线程本地对应物更本地化(即,它们具有更好的信息隐藏属性)。当然,它们可能会被滥用,但大多数其他编程工具也是如此……</p>

请参阅Joshua Bloch的此链接

于 2013-07-31T12:04:33.597 回答
1

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

于 2014-04-25T18:01:06.777 回答
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)。

JavaX 中的示例程序。

最后一点:我的系统 ( JavaX ) 还跟踪所有 WeakHashMap 并定期清理它们,因此最后一个极不可能的漏洞被堵住了(长期存在的 WeakHashMap 从未被查询,但仍有陈旧的条目)。

于 2017-10-26T06:36:07.853 回答
0

当 ThreadLocal 始终存在时会导致内存泄漏。如果 ThreadLocal 对象可以被 GC,则不会导致内存泄漏。因为 ThreadLocalMap 中的 entry 扩展了 WeakReference,所以 ThreadLocal 对象被 GC 后,entry 就会被 GC。

下面的代码创建了很多 ThreadLocal 并且它永远不会内存泄漏,并且 main 的线程始终处于活动状态。

// -XX:+PrintGCDetails -Xms100m -Xmx100m 
public class Test {

    public static long total = 1000000000;
    public static void main(String[] args) {
        for(long i = 0; i < total; i++) {
            // give GC some time
            if(i % 10000 == 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            ThreadLocal<Element> tl = new ThreadLocal<>();
            tl.set(new Element(i));
        }
    }
}

class Element {
    private long v;
    public Element(long v) {
        this.v = v;
    }
    public void finalize() {
        System.out.println(v);
    }
}
于 2018-09-03T10:34:34.363 回答
0

下面的代码,for迭代中的实例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);
        }
    }
}
于 2017-05-10T05:49:42.410 回答