不久前我写过一篇关于ThreadLocal用法的文章,但最近项目上出现了Memory Leak,调查后发现可能与ThreadLocal的使用有关,在此对ThreadLocal的使用作一些补充。
在ThreadLocal内部,其实是通过一个Map(类似Map<Thread, Object>)来保存各个线程独立的变量的,但是这个map有一点特殊,它对线程的引用是弱引用WeakReference(如果一个对象只被弱引用相联,那么GC就可以回收这个对象),这说明当线程执行结果后,即使没有显式的调用ThreadLocal.remove方法,GC也可以回收该线程在ThreadLocal中存放的独立对象了。
我们先看一个简单的例子:
public class App {
public static void main(String[] args) throws Exception {
final ThreadLocal<Obj> local = new ThreadLocal<Obj>();
Thread t = new Thread() {
public void run() {
local.set(new Obj());
}
};
t.start();
while(true) {
System.gc();
TimeUnit.SECONDS.sleep(1);
}
}
}
class Obj {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(this + " finalized.");
}
}
线程t开始执行时创建了一个Obj对象,随后把该对象放入ThreadLocal中,之后线程t执行结束。之后便会
输出Obj@721cdeff finalized,说明Obj对象被GC回收,这与我们上面的分析是一致的。
我们对程序稍作修改,再来看看:
public class App {
public static void main(String[] args) throws Exception {
final ThreadLocal<Obj> local = new ThreadLocal<Obj>();
ExecutorService exec = Executors.newFixedThreadPool(2);
Thread t = new Thread() {
public void run() {
local.set(new Obj());
}
};
exec.execute(t);
while(true) {
System.gc();
TimeUnit.SECONDS.sleep(1);
}
}
}
class Obj {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(this + "finalized.");
}
}
与之前的例子不同的地方是这里使用了线程池来执行线程,这样当线程执行完后并没有被销毁,而是还给了线程池。正因为此ThreadLocal Map中为该线程保存的entry不会被GC回收,也就是说上面这个例子不会有任何输出,Obj对象会在Heap中一直存在。
可以想象下在一个web server环境下,为了提高对请求的响应,大部分web server(比如tomcat)都是预先创建一个线程池。当有请求到来时,就从线程池中取出一个线程来处理请求,之后再将线程放回线程池,也就是说这些线程至始至终都不会被销毁。那如果像上面的例子一样在Web环境下错误地使用了ThreadLocal会带来什么后果呢?
我们再看一个例子:
public class App {
public static void main(String[] args) throws Exception {
final ThreadLocal<Object> local = new ThreadLocal<Object>();
ExecutorService exec = Executors.newFixedThreadPool(2);
Thread t = new Thread() {
public void run() {
local.set(App.createObj());
}
};
exec.execute(t);
while(true) {
System.gc();
TimeUnit.SECONDS.sleep(1);
}
}
public static Object createObj() {
try {
CustomClassLoader cl =
new CustomClassLoader(new URL("file:///Users/ouyang/Develop/eclipse/workspace/Test/bin/"));
Class<?> clazz = cl.loadClass("App$Obj");
return clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
public static class Obj {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println(this + "finalized.");
}
}
}
class CustomClassLoader extends URLClassLoader {
public CustomClassLoader(URL... urls) {
super(urls, null);
}
@Override
protected void finalize() {
System.out.println("*** CustomClassLoader finalized!");
}
}
这个例子在之前例子的基础上,修改了Obj对象的创建,这次我们使用一个自定义的ClassLoader来加载和创建Obj对象。同样的,这个例子不会有任何的输出,
Obj对象不能被GC回收,从而导致加载他的CustomClassLoader对象不能被回收,更要命的是其它被CustomClassLoader加载的类啊、静态数据对象等等,都不能被GC回收,甚至是在undeploy应用的时候都不能被回收。只要web server不重启,每一次重新布暑应用都将加大这些无效类、静态数据所占用的空间。从而造成Permgen Leak和Memory Leak。
所以,必须在线程执行结束前,调用ThreadLocal的remove方法显式的删除对独立对象的强引用。