不久前我写过一篇关于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执行结束。之后便会输出[email protected] 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方法显式的删除对独立对象的强引用。