简体   繁体   English

ThreadLocal和内存泄漏

[英]ThreadLocal & Memory Leak

It is mentioned at multiple posts: improper use of ThreadLocal causes Memory Leak. 在多个帖子中提到:不正确使用ThreadLocal会导致内存泄漏。 I am struggling to understand how Memory Leak would happen using ThreadLocal . 我正在努力理解使用ThreadLocal如何发生内存泄漏。

The only scenario I have figured out it as below: 我发现它的唯一情况如下:

A web-server maintains a pool of Threads (eg for servlets). Web服务器维护一个线程池(例如,用于servlet)。 Those threads can create memory leak if the variables in ThreadLocal are not removed because Threads do not die. 如果ThreadLocal中的变量未被删除,那么这些线程可能会创建内存泄漏,因为线程没有死亡。

This scenario does not mention "Perm Space" memory leak. 这种情况没有提到“Perm Space”内存泄漏。 Is that the only (major) use case of memory leak? 这是内存泄漏的唯一(主要)用例吗?

PermGen exhaustions in combination with ThreadLocal are often caused by classloader leaks . ThreadLocal结合使用的PermGen耗尽通常是由类加载器泄漏引起的。

An example: 一个例子:
Imagine an application server which has a pool of worker threads . 想象一下具有工作线程池的应用服务器。
They will be kept alive until application server termination. 它们将保持活动状态直到应用程序服务器终止
A deployed web application uses a static ThreadLocal in one of its classes in order to store some thread-local data, an instance of another class (lets call it SomeClass ) of the web application. 部署的Web应用程序在其某个类中使用静态 ThreadLocal ,以便存储一些线程本地数据,即Web应用程序的另一个类(让我们称之为SomeClass )的实例。 This is done within the worker thread (eg this action originates from a HTTP request ). 这是在工作线程内完成的(例如,此操作源自HTTP请求 )。

Important: 重要:
By definition , a reference to a ThreadLocal value is kept until the "owning" thread dies or if the ThreadLocal itself is no longer reachable. 根据定义 ,保持对ThreadLocal 的引用,直到“拥有”线程死亡或者ThreadLocal本身不再可访问为止。

If the web application fails to clear the reference to the ThreadLocal on shutdown , bad things will happen: 如果Web应用程序在关闭时 无法清除对 ThreadLocal 的引用 ,则会发生错误:
Because the worker thread will usually never die and the reference to the ThreadLocal is static, the ThreadLocal value still references the instance of SomeClass , a web application's class - even if the web application has been stopped! 因为工作线程通常永远不会死并且对ThreadLocal的引用是静态的,所以ThreadLocal仍然引用了Web应用程序类SomeClass的实例 - 即使Web应用程序已被停止!

As a consequence, the web application's classloader cannot be garbage collected , which means that all classes (and all static data) of the web application remain loaded (this affects the PermGen memory pool as well as the heap). 因此,Web应用程序的类加载器无法进行垃圾回收 ,这意味着Web应用程序的所有类 (以及所有静态数据)都会保持加载状态 (这会影响PermGen内存池以及堆)。
Every redeployment iteration of the web application will increase permgen (and heap) usage. Web应用程序的每次重新部署迭代都会增加permgen(和堆)的使用。

=> This is the permgen leak =>这是permgen泄漏

One popular example of this kind of leak is this bug in log4j (fixed in the meanwhile). 这种泄漏的一个流行的例子是log4j中的这个错误 (同时修复)。

The accepted answer to this question, and the "severe" logs from Tomcat about this issue are misleading. 这个问题的公认答案以及Tomcat关于这个问题的“严重”日志都是误导性的。 The key quote there is: 关键的话是:

By definition, a reference to a ThreadLocal value is kept until the "owning" thread dies or if the ThreadLocal itself is no longer reachable. 根据定义,保持对ThreadLocal值的引用,直到“拥有”线程死亡或者ThreadLocal本身不再可访问为止 [My emphasis]. [我的重点]。

In this case the only references to the ThreadLocal are in the static final field of a class that has now become a target for GC, and the reference from the worker threads. 在这种情况下,对ThreadLocal的唯一引用位于类的静态final字段中,该类现在已成为GC的目标,以及来自工作线程的引用。 However, the references from the worker threads to the ThreadLocal are WeakReferences ! 但是,从工作线程到ThreadLocal的引用WeakReferences

The values of a ThreadLocal are not weak references, however. 但是,ThreadLocal的值不是弱引用。 So, if you have references in the values of a ThreadLocal to application classes, then these will maintain a reference to the ClassLoader and prevent GC. 因此,如果您在ThreadLocal的中引用了应用程序类,那么这些将保留对ClassLoader的引用并阻止GC。 However, if your ThreadLocal values are just integers or strings or some other basic object type (eg, a standard collection of the above), then there should not be a problem (they will only prevent GC of the boot/system classloader, which is never going to happen anyway). 但是,如果你的ThreadLocal值只是整数或字符串或其他一些基本对象类型(例如,上面的标准集合),那么应该没有问题(它们只会阻止引导/系统类加载器的GC,这是永远不会发生)。

It is still good practice to explicitly clean up a ThreadLocal when you are done with it, but in the case of the cited log4j bug the sky was definitely not falling (as you can see from the report, the value is an empty Hashtable). 当你完成它时,明确地清理ThreadLocal仍然是一个好习惯,但是在引用的log4j bug的情况下,天空肯定没有下降(正如你从报告中看到的那样,值是一个空的Hashtable)。

Here is some code to demonstrate. 这是一些要演示的代码。 First, we create a basic custom classloader implementation with no parent that prints to System.out on finalization: 首先,我们创建一个基本的自定义类加载器实现,没有父项在完成时打印到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!");
    }
}

We then define a driver application which creates a new instance of this class loader, uses it to load a class with a ThreadLocal and then remove the reference to the classloader allowing it to be GC'ed. 然后我们定义一个驱动程序应用程序,它创建这个类加载器的新实例,使用它来加载一个带有ThreadLocal的类,然后删除对类加载器的引用,允许它进行GC。 Firstly, in the case where the ThreadLocal value is a reference to a class loaded by the custom classloader: 首先,在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());
        }
    }
}

When we run this, we can see that the CustomClassLoader is indeed not garbage collected (as the thread local in the main thread has a reference to a Foo instance that was loaded by our custom classloader): 当我们运行它时,我们可以看到CustomClassLoader确实没有被垃圾收集(因为主线程中的本地线程具有对我们的自定义类加载器加载的Foo实例的引用):

$ java Main
ClassLoader: CustomClassLoader@7a6d084b

However, when we change the ThreadLocal to instead contain a reference to a simple Integer rather than a Foo instance: 但是,当我们更改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());
    }
}

Then we see that the custom classloader is now garbage collected (as the thread local on the main thread only has a reference to an integer loaded by the system classloader): 于是我们看到,自定义加载器现在垃圾回收(如线程本地主线程只需要由系统类加载器加载一个整数参考):

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

(The same is true with Hashtable). (Hashtable也是如此)。 So in the case of log4j they didn't have a memory leak or any kind of bug. 所以在log4j的情况下,他们没有内存泄漏或任何类型的错误。 They were already clearing the Hashtable and this was sufficient to ensure GC of the classloader. 他们已经清除了Hashtable,这足以确保类加载器的GC。 IMO, the bug is in Tomcat, which indiscriminately logs these "SEVERE" errors on shutdown for all ThreadLocals that have not been explicitly .remove()d, regardless of whether they hold a strong reference to an application class or not. IMO,这个bug出现在Tomcat中,它会在关闭所有未明确的.stove()d的ThreadLocals时不加选择地记录这些“SEVERE”错误,无论它们是否拥有对应用程序类的强引用。 It seems that at least some developers are investing time and effort on "fixing" phantom memory leaks on the say-so of the sloppy Tomcat logs. 似乎至少有一些开发人员正在投入时间和精力来“修复”幻想内存泄漏事件。

There is nothing inherently wrong with thread locals: They do not cause memory leaks. 线程本地没有任何内在错误:它们不会导致内存泄漏。 They are not slow. 他们并不慢。 They are more local than their non-thread-local counterparts (ie, they have better information hiding properties). 它们比非线程本地对应物更本地化(即,它们具有更好的信息隐藏属性)。 They can be misused, of course, but so can most other programming tools… 当然,它们可能被滥用,但大多数其他编程工具也是如此......

Refer to this link by Joshua Bloch 请参阅Joshua Bloch的链接

The previous posts explain the problem but don't provide any solution. 以前的帖子解释了问题,但没有提供任何解决方案。 I found that there is no way to "clear" a ThreadLocal. 我发现没有办法“清除”ThreadLocal。 In a container environment where I'm handling requests, I finally just called .remove() at the end of every request. 在我处理请求的容器环境中,我最终在每个请求结束时调用了.remove()。 I realize that could be problematic using container managed transactions. 我意识到使用容器管理的事务可能会有问题。

Below code, the instance t in the for iteration can not be GC. 在代码下面,for迭代中的实例t不能是GC。 This may be a example of ThreadLocal & Memory Leak 这可能是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);
        }
    }
}

Here is an alternative to ThreadLocal that doesn't have the memory leak problem: 这是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);
  }
}

Note: There is a new memory leak scenario, but it is highly unlikely and can be avoided by following a simple guide line. 注意:存在新的内存泄漏情况,但这种情况极不可能,可以通过遵循简单的指导原则来避免。 The scenario is keeping a strong reference to a Thread object in a BetterThreadLocal. 该场景是对BetterThreadLocal中的Thread对象的强引用。

I never keep strong references to threads anyways because you always want to allow the thread to be GC'd when its work is done... so there you go: a memory leak-free ThreadLocal. 我永远不会保持对线程的强引用,因为你总是希望在它的工作完成时允许线程进行GC ...所以你去:一个无内存漏洞的ThreadLocal。

Someone should benchmark this. 有人应该对此进行基准测试 I expect it to be about as fast as Java's ThreadLocal (both essentially do a weak hash map lookup, just one looks up the thread, the other the ThreadLocal). 我希望它与Java的ThreadLocal一样快(两者都基本上做了一个弱的哈希映射查找,只有一个查找线程,另一个查找ThreadLocal)。

Sample program in JavaX. JavaX中的示例程序。

And a final note: My system ( JavaX ) also keeps track of all WeakHashMaps and cleans them up regularly, so the last super-unlikely hole is plugged (long-lived WeakHashMaps that are never queried, but still have stale entries). 最后一点:我的系统( JavaX )也会跟踪所有WeakHashMaps并定期清理它们,因此插入了最后一个非常不可能的漏洞(永远不会查询的长期WeakHashMaps,但仍然有过时的条目)。

Memory leak is caused when ThreadLocal is always existing. 当ThreadLocal始终存在时,会导致内存泄漏。 If ThreadLocal object could be GC, it will not cause memory leak. 如果ThreadLocal对象可能是GC,则不会导致内存泄漏。 Because the entry in ThreadLocalMap extends WeakReference, the entry will be GC after ThreadLocal object is GC. 因为ThreadLocalMap中的条目扩展了WeakReference,所以在ThreadLocal对象为GC之后,该条目将是GC。

Below code create a lot of ThreadLocal and it never Memory leak and the thread of main is always live. 下面的代码创建了很多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