繁体   English   中英

ThreadLocal和内存泄漏

[英]ThreadLocal & Memory Leak

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

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

Web服务器维护一个线程池(例如,用于servlet)。 如果ThreadLocal中的变量未被删除,那么这些线程可能会创建内存泄漏,因为线程没有死亡。

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

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

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

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

如果Web应用程序在关闭时 无法清除对 ThreadLocal 的引用 ,则会发生错误:
因为工作线程通常永远不会死并且对ThreadLocal的引用是静态的,所以ThreadLocal仍然引用了Web应用程序类SomeClass的实例 - 即使Web应用程序已被停止!

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

=>这是permgen泄漏

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

这个问题的公认答案以及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!");
    }
}

然后我们定义一个驱动程序应用程序,它创建这个类加载器的新实例,使用它来加载一个带有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 finalized!

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

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

请参阅Joshua Bloch的链接

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

在代码下面,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);
        }
    }
}

这是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 ...所以你去:一个无内存漏洞的ThreadLocal。

有人应该对此进行基准测试 我希望它与Java的ThreadLocal一样快(两者都基本上做了一个弱的哈希映射查找,只有一个查找线程,另一个查找ThreadLocal)。

JavaX中的示例程序。

最后一点:我的系统( JavaX )也会跟踪所有WeakHashMaps并定期清理它们,因此插入了最后一个非常不可能的漏洞(永远不会查询的长期WeakHashMaps,但仍然有过时的条目)。

当ThreadLocal始终存在时,会导致内存泄漏。 如果ThreadLocal对象可能是GC,则不会导致内存泄漏。 因为ThreadLocalMap中的条目扩展了WeakReference,所以在ThreadLocal对象为GC之后,该条目将是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);
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM