在最近一個項目中,在項目發佈以後,發現系統中有內存泄漏問題。表象是堆內存隨着系統的運行時間緩慢增加,一直沒有辦法經過gc來回收,最終於致使堆內存耗盡,內存溢出。開始是懷疑ThreadLocal的問題,由於在項目中,大量使用了線程的ThreadLocal保存線程上下文信息,在正常狀況下,在線程開始的時候設置線程變量,在線程結束的時候,須要清除線程上下文信息,若是線程變量沒有清除,會致使線程中保存的對象沒法釋放。java
從這個正常的狀況來看,假設沒有清除線程上下文變量,那麼在線程結束的時候(線程銷燬),線程上下文變量所佔用的內存會隨着線程的銷燬而被回收。至少從程序設計者角度來看,應該如此。實際狀況下是怎麼樣,須要進行測試。web
可是對於web類型的應用,爲了不產生大量的線程產生堆棧溢出(默認狀況下一個線程會分配512K的棧空間),都會採用線程池的設計方案,對大量請求進行負載均衡。因此實際應用中,通常都會是線程池的設計,處理業務的線程數通常都在200如下,即便全部的線程變量都沒有清理,那麼理論上會出現線程保持的變量最大數是200,若是線程變量所指示的對象佔用比較少(小於10K),200個線程最多隻有2M(200*10K)的內存沒法進行回收(由於線程池線程是複用的,每次使用以前,都會重新設置新的線程變量,那麼老的線程變量所指示的對象沒有被任何對象引用,會自動被垃圾回收,只有最後一次線程被使用的狀況下,纔沒法進行回收)。負載均衡
以上只是理論上的分析,那麼實際狀況下如何了,我寫了一段代碼進行實驗。測試
處理器名稱: Intel Core i7 2.3 GHz 4核this
內存: 16 GBspa
操做系統:OS X 10.8.2操作系統
java版本:"1.7.0_04-ea"線程
-Xms128M -Xmx512M -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -Xloggc:gc.log設計
測試代碼:Test.java 日誌
import java.io.BufferedReader; import java.io.InputStreamReader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Test { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); int testCase= Integer.parseInt(br.readLine()); br.close(); switch(testCase){ // 測試狀況1. 無線程池,線程不休眠,而且清除thread_local 裏面的線程變量;測試結果:無內存溢出 case 1 :testWithThread(true, 0); break; // 測試狀況2. 無線程池,線程不休眠,沒有清除thread_local 裏面的線程變量;測試結果:無內存溢出 case 2 :testWithThread(false, 0); break; // 測試狀況3. 無線程池,線程休眠1000毫秒,清除thread_local裏面的線程的線程變量;測試結果:無內存溢出,可是新生代內存總體使用高 case 3 :testWithThread(false, 1000); break; // 測試狀況4. 無線程池,線程永久休眠(設置最大值),清除thread_local裏面的線程的線程變量;測試結果:無內存溢出 case 4 :testWithThread(true, Integer.MAX_VALUE); break; // 測試狀況5. 有線程池,線程池大小50,線程不休眠,而且清除thread_local 裏面的線程變量;測試結果:無內存溢出 case 5 :testWithThreadPool(50,true,0); break; // 測試狀況6. 有線程池,線程池大小50,線程不休眠,沒有清除thread_local 裏面的線程變量;測試結果:無內存溢出 case 6 :testWithThreadPool(50,false,0); break; // 測試狀況7. 有線程池,線程池大小50,線程無限休眠,而且清除thread_local 裏面的線程變量;測試結果:無內存溢出 case 7 :testWithThreadPool(50,true,Integer.MAX_VALUE); break; // 測試狀況8. 有線程池,線程池大小1000,線程無限休眠,而且清除thread_local 裏面的線程變量;測試結果:無內存溢出 case 8 :testWithThreadPool(1000,true,Integer.MAX_VALUE); break; default :break; } } public static void testWithThread(boolean clearThreadLocal, long sleepTime) { while (true) { try { Thread.sleep(100); new Thread(new TestTask(clearThreadLocal, sleepTime)).start(); } catch (Exception e) { e.printStackTrace(); } } } public static void testWithThreadPool(int poolSize,boolean clearThreadLocal, long sleepTime) { ExecutorService service = Executors.newFixedThreadPool(poolSize); while (true) { try { Thread.sleep(100); service.execute(new TestTask(clearThreadLocal, sleepTime)); } catch (Exception e) { e.printStackTrace(); } } } public static final byte[] allocateMem() { // 這裏分配一個1M的對象 byte[] b = new byte[1024 * 1024]; return b; } static class TestTask implements Runnable { /** 是否清除上下文參數變量 */ private boolean clearThreadLocal; /** 線程休眠時間 */ private long sleepTime; public TestTask(boolean clearThreadLocal, long sleepTime) { this.clearThreadLocal = clearThreadLocal; this.sleepTime = sleepTime; } public void run() { try { ThreadLocalHolder.set(allocateMem()); try { // 大於0的時候才休眠,不然不休眠 if (sleepTime > 0) { Thread.sleep(sleepTime); } } catch (InterruptedException e) { } } finally { if (clearThreadLocal) { ThreadLocalHolder.clear(); } } } } }
ThreadLocalHolder.java
public class ThreadLocalHolder { public static final ThreadLocal<Object> threadLocal = new ThreadLocal<Object>(); public static final void set(byte [] b){ threadLocal.set(b); } public static final void clear(){ threadLocal.set(null); } }
無線程池的狀況:測試用例1-4
下面是測試用例1 的垃圾回收日誌
下面是測試用例2 的垃圾回收日誌
對比分析測試用例1 和 測試用例2 的GC日誌,發現基本上都差很少,說明是否清楚線程上下文變量不影響垃圾回收,對於無線程池的狀況下,不會形成內存泄露
對於測試用例3,因爲業務線程sleep 一秒鐘,會致使業務系統中有產生大量的阻塞線程,理論上新生代內存會比較高,可是會保持到必定的範圍,不會緩慢增加,致使內存溢出,經過分析了測試用例3的gc日誌,發現符合理論上的分析,下面是測試用例3的垃圾回收日誌
經過上述日誌分析,發現老年代產生了一次垃圾回收,多是開始大量線程休眠致使內存沒法釋放,這一部分線程持有的線程變量會在從新喚醒以後運行結束被回收,新生代的內存內存一直維持在4112K,也就是4個線程持有的線程變量。
對於測試用例4,因爲線程一直sleep,沒法對線程變量進行釋放,致使了內存溢出。
有線程池的狀況:測試用例5-8
對於測試用例5,開設了50個工做線程,每次使用線程完成以後,都會清除線程變量,垃圾回收日誌和測試用例1以及測試用例2同樣。
對於測試用例6,也開設了50個線程,可是使用完成以後,沒有清除線程上下文,理論上會有50M內存沒法進行回收,經過垃圾回收日誌,符合咱們的語氣,下面是測試用例6的垃圾回收日誌
經過日誌分析,發現老年代回收比較頻繁,主要是由於50個線程持有的50M空間一直沒法完全進行回收,而新生代空間不夠(咱們設置的是128M內存,新生代大概36M左右)。全部總體內存的使用量確定一直在50M之上。
對於測試用例7,因爲工做線程最多50個,即便線程一直休眠,再短期內也不會致使內存溢出,長時間的狀況下會出現內存溢出,這主要是由於任務隊列空間沒有限制,和有沒有清除線程上下文變量沒有關係,若是咱們使用的有限隊列,就不會出現這個問題。
對於測試用例8,因爲工做線程有1000個,致使至少1000M的堆空間被使用,因爲咱們設置的最大堆是512M,致使結果溢出。系統的堆空間會從開始的128M逐步增加到512M,最後致使溢出,從gc日誌來看,也符合理論上的判斷。因爲gc日誌比較大,就不在貼出來了。
因此從上面的測試狀況來看,線上上下文變量是否致使內存泄露,是須要區分狀況的,若是線程變量所佔的空間的比較小,小於10K,是不會出現內存泄露的,致使內存溢出的。若是線程變量所佔的空間比較大,大於1M的狀況下,出現的內存泄露和內存溢出的狀況比較大。以上只是jdk1.7版本狀況下的分析,我的認爲jdk1.6版本的狀況和1.7應該差很少,不會有太大的差異。
-----------------------下面是對ThreadLocal的分析-------------------------------------
對於ThreadLocal的概念,不少人都是比較模糊的,只知道是線程本地變量,而具體這個本地變量是什麼含義,有什麼做用,如何使用等不少java開發工程師都不知道如何進行使用。從JDK的對ThreadLocal的解釋來看
該類提供了線程局部 (thread-local) 變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其 get 或 set 方法)的每一個線程都有本身的局部變量, 它獨立於變量的初始化副本。ThreadLocal 實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。 |
ThreadLocal有一個ThreadLocalMap靜態內部類,你能夠簡單理解爲一個MAP,這個‘Map’爲每一個線程複製一個變量的‘拷貝’存儲其中。每個內部線程都有一個ThreadLocalMap對象。
當線程調用ThreadLocal.set(T object)方法設置變量時,首先獲取當前線程引用,而後獲取線程內部的ThreadLocalMap對象,設置map的key值爲threadLocal對象,value爲參數中的object。
當線程調用ThreadLocal.get()方法獲取變量時,首先獲取當前線程引用,以threadLocal對象爲key去獲取響應的ThreadLocalMap,若是此‘Map’不存在則初始化一個,不然返回其中的變量。
也就是說每一個線程內部的 ThreadLocalMap對象中的key保存的threadLocal對象的引用,從ThreadLocalMap的源代碼來看,對threadLocal的對象的引用是WeakReference,也就是弱引用。
下面一張圖描述這三者的總體關係
對於一個正常的Map來講,咱們通常會調用Map.clear方法來清空map,這樣map裏面的全部對象就會釋放。調用map.remove(key)方法,會移除key對應的對象整個entry,這樣key和value 就不會任何對象引用,被java虛擬機回收。
而Thread對象裏面的ThreadLocalMap裏面的key是ThreadLocal的對象的弱引用,若是ThreadLocal對象會回收,那麼ThreadLocalMap就沒法移除其對應的value,那麼value對象就沒法被回收,致使內存泄露。可是若是thread運行結束,整個線程對象被回收,那麼value所引用的對象也就會被垃圾回收。
什麼狀況下 ThreadLocal對象會被回收了,典型的就是ThreadLocal對象做爲局部對象來使用或者每次使用的時候都new了一個對象。因此通常狀況下,ThreadLocal對象都是static的,確保不會被垃圾回收以及任什麼時候候線程都可以訪問到這個對象。
寫了下面一段代碼進行測試,發現兩個方法都沒有致使內存溢出,對於沒有使用線程池的方法來講,由於每次線程運行完就退出了,Map裏面引用的全部對象都會被垃圾回收,因此沒有關係,可是爲何線程池的方案也沒有致使內存溢出了,主要緣由是ThreadLocal.set方法的實現,會作一個將Key== null 的元素清理掉的工做。致使線程以前因爲ThreadLocal對象回收以後,ThreadLocalMap中的value 也會被回收,可見設計者也注意到這個地方可能出現內存泄露,爲了防止這種狀況發生,從而清空ThreadLocalMap中null爲空的元素。
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadLocalLeakTest { public static void main(String[] args) { // 若是控制線程池的大小爲50,不會致使內存溢出 testWithThreadPool(50); // 也不會致使內存泄露 testWithThread(); } static class TestTask implements Runnable { public void run() { ThreadLocal tl = new ThreadLocal(); // 確保threadLocal爲局部對象,在退出run方法以後,沒有任何強引用,能夠被垃圾回收 tl.set(allocateMem()); } } public static void testWithThreadPool(int poolSize) { ExecutorService service = Executors.newFixedThreadPool(poolSize); while (true) { try { Thread.sleep(100); service.execute(new TestTask()); } catch (Exception e) { e.printStackTrace(); } } } public static void testWithThread() { try { Thread.sleep(100); } catch (InterruptedException e) { } new Thread(new TestTask()).start(); } public static final byte[] allocateMem() { // 這裏分配一個1M的對象 byte[] b = new byte[1024 * 1024 * 1]; return b; } }