在多个帖子中提到:ThreadLocal
的不当使用会导致内存泄漏。我正在努力了解使用ThreadLocal
将如何发生内存泄漏。
我唯一想到的情况如下:
Web服务器维护一个Threads池(例如,用于servlet)。如果
ThreadLocal
中的变量未被删除,那么这些线程可能会造成内存泄漏,因为线程没有死亡。
此方案未提及“Perm Space”内存泄漏。这是内存泄漏的唯一(主要)用例吗?
答案 0 :(得分:66)
ThreadLocal
的 PermGen耗尽通常是由类加载程序泄漏引起的。
一个例子:
想象一下一个应用服务器,它有一个工作线程池
在应用程序服务器终止之前,它们将保持活动状态
部署的Web应用程序在其中一个类中使用静态 ThreadLocal
,以便存储一些线程本地数据,另一个类的实例(让我们称之为SomeClass
)网络应用程序。这是在工作线程中完成的(例如,此操作源自 HTTP请求)。
重要:强>
By definition,保留对ThreadLocal
值的引用,直到“拥有”线程死亡或者ThreadLocal本身不再可用为止。
如果网络应用无法在关机时清除<{1}} 的参考,则会出现问题:
因为工作线程通常永远不会死,并且对ThreadLocal
的引用是静态的,ThreadLocal
值仍然引用 ThreadLocal
的实例,这是一个Web应用程序的类 - 即使Web应用程序已停止!
因此,Web应用程序的类加载器无法进行垃圾回收,这意味着 Web应用程序的所有类(以及所有静态数据)仍然加载(这会影响PermGen内存池和堆)。
Web应用程序的每次重新部署迭代都会增加permgen(和堆)的使用。
的 =&GT;这是permgen泄漏
这种泄漏的一个常见例子是log4j this bug(同时修复)。
答案 1 :(得分:27)
此问题的已接受答案以及Tomcat关于此问题的“严重”日志具有误导性。关键的话是:
根据定义,保持对ThreadLocal值的引用,直到“拥有”线程死亡或者ThreadLocal本身不再可达。 [我的重点]。
在这种情况下,对ThreadLocal的唯一引用位于类的静态final字段中,该类现在已成为GC的目标,以及来自工作线程的引用。但是,从工作线程到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加载。首先,在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”错误,无论它们是否拥有对应用程序类的强引用。似乎至少有一些开发人员正在投入时间和精力来“修复”模糊内存泄漏的问题 - 就像邋tom的Tomcat日志一样。
答案 2 :(得分:1)
线程本地没有任何内在错误:它们不会导致内存泄漏。他们并不慢。它们比非线程本地对应物更本地化(即,它们具有更好的信息隐藏属性)。当然,它们可能被滥用,但大多数其他编程工具也可以被滥用......
请参阅Joshua Bloch的link
答案 3 :(得分:0)
之前的帖子解释了问题,但没有提供任何解决方案。我发现没有办法“清除”ThreadLocal。在我处理请求的容器环境中,我最终在每个请求结束时调用了.remove()。我意识到使用容器管理的事务可能会有问题。
答案 4 :(得分: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);
}
}
}
答案 5 :(得分: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 ...所以你去:内存泄漏的ThreadLocal。
有人应该对此进行基准测试。我希望它与Java的ThreadLocal一样快(两者都基本上做一个弱的哈希映射查找,只有一个查找线程,另一个查找ThreadLocal)。
最后一点说明:我的系统(JavaX)也会跟踪所有WeakHashMaps并定期清理它们,因此插入最后一个不太可能的漏洞(永远不会被查询的长期WeakHashMaps,但仍然有过时的条目)。
答案 6 :(得分:0)
当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);
}
}