乾貨分享:淺談記憶體洩露

HitTwice發表於2018-08-08

作者:孤獨煙 來源:微信公眾號(打雜的ZRJ)

原文連結:https://mp.weixin.qq.com/s/TEeuqi3PXfpj0zvDKrX_vQ 孤獨煙

前言

這個話題已經是老生常談了,之所以又被我拎出來,是因為博主隔壁的一個童鞋最近寫了一篇叫做《ThreadLocal記憶體洩露》的文章,我就不上鍊接了,因為寫的實在是。。 (省略一萬字)
重點是寫完後,還被我問懵了。出於人道主義關懷,博主很不要臉的再寫一篇。

正文

定義

首先,我們要先談一下定義,因為一堆人搞不懂記憶體溢位和記憶體洩露的區別。
記憶體溢位(OutOfMemory): 你只有十塊錢,我卻找你要了一百塊。對不起啊,我沒有這麼多錢。 (給不起)
記憶體洩露(MemoryLeak): 你有十塊錢,我找你要一塊。但是無恥的博主,不把錢還你了。 (沒退還)
關係: 多次的記憶體洩露,會導致記憶體溢位。(博主不要臉的找你多要幾次錢,你就沒錢了,就是這個道理。)

危害

ok,大家在專案中有沒遇到過java程式越來越卡的情況。
因為記憶體洩露,會導致頻繁的 Full GC ,而 Full GC   又會造成程式停頓,最後Crash了。因此,你會感覺到你的程式越來越卡,越來越卡,然後你就被產品經理鄙視了。順便提一下,我們之所以JVM調優,就是為了減少 Full GC 的出現。
我記得,我曾經有一次,就遇到專案剛上線的時候好好的。結果隨著時間的堆積,報了 OutOfMemoryError: PermGen space
說到這個 PermGen space ,突然間,一陣洪荒之力,從博主體內噴湧而出,一定要介紹一下這個方法區,不過點到為止,畢竟這不是在講《jvm從入門到放棄》。
方法區 :出自java虛擬機器規範, 可供 各條執行緒共享 執行時記憶體區域 。它儲存了 每一個類的結構資訊 ,例如執行時常量池( Runtime Constant Pool )、欄位和方法資料、建構函式和普通方法的位元組碼內容。
上面講的是規範,在不同虛擬機器裡頭實現是不一樣的,最典型的就是 永久代(PermGen space) 元空間(Metaspace)

jdk1.8以前: 實現方法區的叫永久代。因為在很久遠以前,java覺得類幾乎是 靜態的 ,並且很少被解除安裝和回收,所以給了一個 永久代 的雅稱。 因此 ,如果你在專案中,發現堆和永久代一直在不斷增長,沒有下降趨勢,回收的速度根本趕不上增長的速度,不用說了,這種情況基本可以確定是記憶體洩露。

jdk1.8以後: 實現方法區的叫元空間。Java覺得對永久代進行調優是很困難的。永久代中的後設資料可能會隨著每一次 Full GC 發生而進行移動。並且為永久代設定空間大小也是很難確定的。 因此 ,java決定將類的後設資料分配在本地記憶體中,元空間的最大可分配空間就是系統可用記憶體空間。這樣,我們就避開了設定永久代大小的問題。 但是 ,這種情況下,一旦發生記憶體洩露,會佔用你的大量本地記憶體。如果你發現,你的專案中本地記憶體佔用率異常高。嗯,這就是記憶體洩露了。

如何排查

(1)透過 jps 查詢java程式id。
(2)透過 top -p [pid] 發現記憶體佔用達到了最大值
(3) jstat -gccause pid 20000   每隔20秒輸出 Full GC 結果
(4)發現 Full GC 次數太多,基本就是記憶體洩露了。生成 dump 檔案,藉助工具分析是哪個物件太多了。基本能定位到問題在哪。

例項

在stackoverflow上,有一個問題,如下所示

I just had an interview, and I was asked to create a memory leak with Java. Needless to say I felt pretty dumb having no clue on how to even start creating one.

大致就是,因為面試需要手寫一段記憶體洩露的程式,然後提問的人突然懵逼了,於是很多大佬紛紛給出回答。
案例一
此例子出自《演算法》(第四版)一書,我簡化了一下

    class stack{    
        Object data[1000];    
        int top = 0;    
        public void push(Object o){        
            data[top++] = o;   
        }    
        public Object pop(Object o){ 
            return data[--top];
        }
    }

當資料從棧裡面彈出來之後,data陣列還一直保留著指向元素的指標。那麼就算你把棧pop空了,這些元素佔的記憶體也不會被回收的。
解決方案就是

    public Object pop(Object o){ 
        Object result = data[--top];
        data[top] = null;
        return result;
    }

案例二
這個其實是一堆例子,這些例子造成記憶體洩露的原因都是類似的,就是 不關閉流 ,具體的,可以是檔案流,socket流,資料庫連線流,等等
具體如下,沒關檔案流

try {
    BufferedReader br = new BufferedReader(new FileReader(inputFile));
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

再比如,沒關閉連線

try {
    Connection conn = ConnectionFactory.getConnection();
    ...
    ...
} catch (Exception e) {
    e.printStacktrace();
}

解決方案就是。。。嗯,大家應該都會。。你敢說你不會調 close() 方法。
案例三
講這個例子前,大家對 ThreadLocal Tomcat 中引起記憶體洩露有了解麼。不過,我要說一下,這個洩露問題,和ThreadLocal本身關係不大,我看了一下官網給的例子,基本都是屬於使用不當引起的。
在Tomcat的官網上,記錄了這個問題。地址是:
不過,官網的這個例子,可能不好理解,我們略作改動。

public class HelloServlet extends HttpServlet{
    private static final long serialVersionUID = 1L;
    static class LocalVariable {
        private Long[] a = new Long[1024 * 1024 * 100];
    }
    final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();
    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        localVariable.set(new LocalVariable());
    }
}

再來看下conf下sever.xml配置

  <!--The connectors can use a shared executor, you can define one or more named thread pools-->
    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" 
        maxThreads="150" minSpareThreads="4"/>

執行緒池最大執行緒為150個,最小執行緒為4個

Tomcat中Connector元件負責接受並處理請求,每來一個請求,就會去執行緒池中取一個執行緒。
在訪問該servlet時, ThreadLocal 變數裡面被新增了 new LocalVariable() 例項,但是沒有被 remove ,這樣該變數就隨著執行緒回到了執行緒池中。另外多次訪問該 servlet 可能用的不是工作執行緒池裡面的同一個執行緒,這會導致工作執行緒池裡面多個執行緒都會存在記憶體洩露。

另外, servlet doGet 方法裡面建立 new LocalVariable() 的時候使用的是 webappclassloader
那麼
LocalVariable 物件沒有釋放 ->   LocalVariable.class 沒有釋放 -> webappclassloader 沒有釋放 ->   webappclassloader 載入的所有類也沒有被釋放,也造成了記憶體洩露。

除此之外,你在 eclipse 中,做一個 reload 操作,工作執行緒池裡面的執行緒還是一直存在的,並且執行緒裡面的 threadLocal 變數並沒有被清理。而 reload 的時候,又會新構建一個 webappclassloader ,重複上述步驟。多reload幾次,就記憶體溢位。
不過Tomcat7.0以後,你每做一次 reload ,會清理工作執行緒池中執行緒的 threadLocals 變數。因此,這個問題在tomcat7.0後,不會存在。

ps: ThreadLocal 的使用在 Tomcat 的服務環境下要注意,並非每次web請求時候程式執行的 ThreadLocal 都是唯一的。 ThreadLocal 的什麼生命週期不等於一次 Request 的生命週期。 ThreadLocal 與執行緒物件緊密繫結的,由於 Tomcat 使用了執行緒池,執行緒是可能存在複用情況。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31473948/viewspace-2199439/,如需轉載,請註明出處,否則將追究法律責任。

相關文章