ThreaLocal內存泄露的問題

在最近一個項目中,在項目發佈以後,發現系統中有內存泄漏問題。表象是堆內存隨着系統的運行時間緩慢增加,一直沒有辦法經過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"線程

  • JVM配置

-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;
    }

}
相關文章
相關標籤/搜索