不久前我寫過一篇關於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方法顯式的刪除對獨立對象的強引用。