反駁:Threadlocal存在內存泄露

最近看到網上的一篇文章,分析說明ThreadLocal是如何內存泄露的. 但我不這麼認爲. ThreadLocal設計的很好,根本不存在內存泄露問題. 本文就結合圖和代碼的例子來驗證個人見解. java

網上的代碼例子廣泛是這樣子的: ide

public class Test {
	public static void main(String[] args) throws InterruptedException {
		ThreadLocal tl = new MyThreadLocal();
		tl.set(new My50MB());
		
		tl=null;
		
		System.out.println("Full GC");
		System.gc();
	}
	
	public static class MyThreadLocal extends ThreadLocal {
		private byte[] a = new byte[1024*1024*1];
		
		@Override
		public void finalize() {
			System.out.println("My threadlocal 1 MB finalized.");
		}
	}
	
	public static class My50MB {
		private byte[] a = new byte[1024*1024*50];
		
		@Override
		public void finalize() {
			System.out.println("My 50 MB finalized.");
		}
	}

}
結果天然打印

Full GC
My threadlocal 1 MB finalized. spa

Thread.sleep 1秒是爲了給GC一個反應的時間. GC優先級低,即便調用了system.gc也不能馬上執行.因此sleep 1秒.

不少人就開始分析了: threadlocal裏面使用了一個存在弱引用的map,當釋放掉threadlocal的強引用之後,map裏面的value卻沒有被回收.而這塊value永遠不會被訪問到了. 因此存在着內存泄露. 最好的作法是將調用threadlocal的remove方法. 線程

說的也比較正確,當value再也不使用的時候,調用remove的確是很好的作法.但內存泄露一說卻不正確. 這是threadlocal的設計的不得已而爲之的問題.  設計

首先,讓咱們看看在threadlocal的生命週期中,都存在哪些引用吧. 看下圖: 實線表明強引用,虛線表明弱引用.
code

每一個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key爲一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每一個key都弱引用指向threadlocal. 像上面code中的例子,當把threadlocal實例tl置爲null之後,沒有任何強引用指向threadlocal實例,因此threadlocal將會被gc回收. 可是,咱們的value卻不能回收,由於存在一條從current thread鏈接過來的強引用. 只有當前thread結束之後, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將所有被GC回收. 對象

從中能夠看出,弱引用只存在於key上,因此key會被回收. 而value還存在着強引用.只有thead退出之後,value的強引用鏈條纔會斷掉. 看下面改進後的例子. 接口

public class Test2 {

	/**
	 * @param args
	 * @throws InterruptedException 
	 */
	public static void main(String[] args) throws InterruptedException {
		new Thread(new Runnable() {

			@Override
			public void run() {
				ThreadLocal tl = new MyThreadLocal();
				tl.set(new My50MB());
				
				tl=null;
				
				System.out.println("Full GC");
				System.gc(); 
				
			}
			
		}).start();
		
		
		System.gc();
		Thread.sleep(1000);
		System.gc();
		Thread.sleep(1000);
		System.gc();
		Thread.sleep(1000);

	}

}
這一次的打印將輸出:

Full GC
My threadlocal 1 MB finalized.
My 50 MB finalized. 生命週期

咱們能夠看到,全部的都回收了.爲何要屢次調用system.gc()? 這和finalize方法的策略有關係. finalize是一個特別低優先級的線程,當執行gc時,若是一個對象須要被回收,先執行它的finalize方法.這意味着,本次gc可能沒法真正回收這個具備finalize方法的對象.留待下次回收. 這裏屢次調用system.gc正是爲了給finalize留些時間.

從上面的例子能夠看出,當線程退出之後,咱們的value被回收了. 這是正確的.這說明內存並無泄露. 棧中還存在着對value的強引用路線.只是因爲thread沒有提供public接口,沒法訪問此value,但咱們可使用反射拿到這個value. 內存

這也是不得已而爲之的設計吧. 總之,若是不想依賴線程的生命週期,那就調用remove方法來釋放value的內存吧. 讓咱們好好思考一下,有什麼辦法能夠在tl=null的時候,也釋放value呢?

相關文章
相關標籤/搜索