「每日分享」有趣易懂的內存洩漏分析與實戰

點擊上方"java全棧技術"關注,每天學習一個java知識點

定義

首先,我們要先談一下定義,因為一堆人搞不懂內存溢出和內存洩露的區別。

內存溢出(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.

大致就是,因為面試需要手寫一段內存洩露的程序,然後提問的人突然懵逼了,於是很多大佬紛紛給出回答。

案例一

此例子出自《算法》(第四版)一書,我簡化了一下

「每日分享」有趣易懂的內存洩漏分析與實戰

當數據從棧裡面彈出來之後,data數組還一直保留著指向元素的指針。那麼就算你把棧pop空了,這些元素佔的內存也不會被回收的。

解決方案就是

「每日分享」有趣易懂的內存洩漏分析與實戰

案例二

這個其實是一堆例子,這些例子造成內存洩露的原因都是類似的,就是不關閉流,具體的,可以是文件流,socket流,數據庫連接流,等等

具體如下,沒關文件流

「每日分享」有趣易懂的內存洩漏分析與實戰

再比如,沒關閉連接

「每日分享」有趣易懂的內存洩漏分析與實戰

解決方案就是:嗯,大家應該都會。再來看下conf下sever.xml配置你敢說你不會調close()方法。

案例三

講這個例子前,大家對ThreadLocal在Tomcat中引起內存洩露有了解麼。不過,我要說一下,這個洩露問題,和ThreadLocal本身關係不大,我看了一下官網給的例子,基本都是屬於使用不當引起的。

在Tomcat的官網上,記錄了這個問題。地址是:https://wiki.apache.org/tomcat/MemoryLeakProtection

不過,官網的這個例子,可能不好理解,我們略作改動。

「每日分享」有趣易懂的內存洩漏分析與實戰

再來看下conf下sever.xml配置

「每日分享」有趣易懂的內存洩漏分析與實戰

線程池最大線程為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使用了線程池,線程是可能存在複用情況。


分享到:


相關文章: