【问题标题】:ThreadLocal & Memory LeakThreadLocal 和内存泄漏
【发布时间】:2013-07-31 21:15:03
【问题描述】:

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

我想出的唯一场景如下:

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

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

【问题讨论】:

  • 你能稍微澄清一下这个问题吗?您是否只想知道 ThreadLocal 是否/如何导致 PermGen 耗尽?
  • 是的。我想知道 - 1. ThreadLocal 如何导致 PermGen 耗尽。 2. 其他任何错误使用ThreadLocal导致内存泄漏的常见场景。
  • java.jiderhamn.se/2012/01/29/…有详细解释作者还提出了一个有趣的防漏库。

标签: java multithreading memory-leaks thread-local permgen


【解决方案1】:

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

请参阅 Joshua Bloch 的 link

【讨论】:

    【解决方案2】:

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

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

    重要提示:
    By definition,参考ThreadLocal 一直保留到“拥有”线程死亡或 ThreadLocal 本身不再可访问。

    如果 Web 应用程序未能清除ThreadLocal 关闭时的引用,则会发生不好的事情:
    因为工作线程通常永远不会死掉并且对ThreadLocal 的引用是静态的,所以ThreadLocal仍然引用SomeClass 的实例,一个Web 应用程序的类- 即使Web 应用程序已停止!

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

    => 这是永久泄漏

    此类泄漏的一个流行示例是 log4j 中的 this bug(同时已修复)。

    【讨论】:

    • 这个(可能是愚蠢的)问题是 - 为什么 Web 服务器在应用程序停止时不尝试终止该应用程序的工作线程?我猜测/假设 Web 服务器会为应用程序创建特定的工作线程。如果 webserver 不杀死这些线程,即使没有 ThreadLocal,线程也会保留在那里。
    • 线程不应该被杀死,它们只应该被通知/中断以便自行温和地终止。此外,线程的创建成本很高,并且通常在同一容器内的多个应用程序之间共享——但这是特定于实现的。但是,一些应用程序服务器会丢弃已停止的 Web 应用程序的所有线程(取决于产品/配置),或定期更新其线程以防止此类泄漏。这也是特定于实现的。看一下tomcat的详细信息:tomcat.apache.org/tomcat-7.0-doc/config/executor.html
    • @MRalwasser:感谢您的精彩解释。很清楚为什么treadlocal变量不会被垃圾回收,但为什么classloader和其他类也不会被垃圾回收?其他类不必对具有线程局部变量的类做任何事情
    • @Victor:问题是 ThreadLocal 的值也不会被垃圾收集。因为该值是关闭的 Web 应用程序的一个实例,所以它的类加载器以及所有其他类也不会被垃圾回收。
    • MRalwasser,完美解释!
    【解决方案3】:

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

    【讨论】:

    • 您能否详细说明使用带有 ThreadLocal 的容器管理事务的问题?
    【解决方案4】:

    这个问题的公认答案以及来自 Tomcat 关于这个问题的“严重”日志具有误导性。关键引用是:

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

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

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

    当你完成一个 ThreadLocal 时,显式清理它仍然是一个好习惯,但是在 the cited 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 主要 类加载器: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 完成!

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

    【讨论】:

    • 有时人们在 threadLocal 值中使用自定义类,甚至没有意识到这一点。例如,当您对线程局部值使用双括号初始化时,可能会发生这种情况,因为双括号初始化将创建一个匿名类。我为线程本地创建了一个修复程序,它修复了您的 Web 应用程序的类加载器泄漏,但保留了对线程本地值的非阻塞访问:github.com/codesinthedark/ImprovedThreadLocal 现在您可以在 ThreadLocal 中使用您的类并且您将没有内存webapp重新部署泄漏
    • 这很有帮助 - 所以我们不应该为这个用例编写答案,或者有办法防止这种内存泄漏。
    • However, if your ThreadLocal values are just integers or strings or some other basic object type (e.g., a standard collection of the above), then there should not be a problem。有人可以解释一下吗???为什么有区别?是因为类加载器吗?
    • @jackycflau JVM 加载的每个类都有一个对加载它的 ClassLoader 的引用 - obj.getClass().getClassLoader()。默认情况下,每个 ClassLoader 都会引用它已加载的每个类。因此,如果您将自定义类存储在由自定义 ClassLoader 加载的 ThreadLocal 中,那么它将防止 ClassLoader 被垃圾收集以及它加载的所有类。但是标准库中的类是由引导类加载器加载的,无论如何都不会被垃圾回收。
    【解决方案5】:

    如下代码,for迭代中的实例t不能被GC。这可能是ThreadLocal &amp; 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);
            }
        }
    }
    

    【讨论】:

      【解决方案6】:

      这里有一个没有内存泄漏问题的 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)。

      Sample program in JavaX.

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

      【讨论】:

      • 这不适用于提出的问题。你仍然有同样的内存泄漏问题,因为线程永远不会被收集到 Java Web 服务器中,因为它们的生命周期独立于 Web 应用程序。从而防止值被收集。如果这些值是应用类加载器加载的类的实例,那么您的应用将永远不会被释放。
      • 不要这样做,它会比JDK ThreadLocal慢得多,因为threadlocals存储在线程对象中,因此不需要任何同步。这个实现会很慢,正如@Talijanac 所说,它不能解决内存泄漏问题。
      • @Taljanac 如果你保持线程的时间过长,你也会遇到 ThreadLocal 的问题。
      【解决方案7】:

      当 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);
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2011-11-25
        • 2011-12-16
        • 2015-12-28
        • 2022-06-10
        • 2014-07-30
        • 2010-10-30
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多