原文:http://blog.csdn.net/rachel_luo/article/details/8990202
前言
在平時工作過程中,有時會遇到OutOfMemoryError,我們知道遇到Error一般表明程式存在著嚴重問題,可能是災難性的。所以找出是什麼原因造成OutOfMemoryError非常重要。現在向大家引薦Eclipse Memory Analyzer tool(MAT),來化解我們遇到的難題。如未說明,本文均使用Java 5.0 on Windows XP SP3環境。
為什麼用MAT
之前的觀點,我認為使用即時profiling/monitoring之類的工具,用一種非常即時的方式來分析哪裡存在記憶體流失是很正確的。年初使用了某profiler工具測試訊息中介軟體中存在的記憶體流失,發現在輸送量很高的時候profiler工具自己也無法響應,這讓人很頭痛。後來瞭解到這樣的工具本身就要消耗效能,且在某些條件下還發現不了泄漏。所以,分析離線資料就非常重要了,MAT正是這樣一款工具。
為何會記憶體溢出
我們知道JVM根據generation(代)來進行GC,根據下圖所示,一共被分為young generation(年輕代)、tenured generation(老年代)、permanent generation(永久代, perm gen),perm gen(或稱Non-Heap 非堆)是個異類,稍後會講到。注意,heap空間不包括perm gen。
絕大多數的對象都在young generation被分配,也在young generation被收回,當young generation的空間被填滿,GC會進行minor collection(次回收),這次回收不涉及到heap中的其他generation,minor collection根據weak generational hypothesis(弱年代假設)來假設young generation中大量的對象都是垃圾需要回收,minor collection的過程會非常快。young generation中未被回收的對象被轉移到tenured generation,然而tenured generation也會被填滿,最終觸發major collection(主回收),這次回收針對整個heap,由於涉及到大量對象,所以比minor collection慢得多。
JVM有三種記憶體回收行程,分別是throughput collector,用來做並行young generation回收,由參數-XX:+UseParallelGC啟動;concurrent low pause collector,用來做tenured generation並發回收,由參數-XX:+UseConcMarkSweepGC啟動;incremental low pause collector,可以認為是預設的記憶體回收行程。不建議直接使用某種記憶體回收行程,最好讓JVM自己決斷,除非自己有足夠的把握。
Heap中各generation空間是如何劃分的。通過JVM的-Xmx=n參數可指定最大heap空間,而-Xms=n則是指定最小heap空間。在JVM初始化的時候,如果最小heap空間小於最大heap空間的話,如上圖所示JVM會把未用到的空間標註為Virtual。除了這兩個參數還有-XX:MinHeapFreeRatio=n和 -XX:MaxHeapFreeRatio=n來分別控制最大、最小的剩餘空間與使用中的物件之比例。在32位Solaris SPARC作業系統下,預設值如下,在32位windows xp下,預設值也差不多。
參數 |
預設值 |
MinHeapFreeRatio |
40 |
MaxHeapFreeRatio |
70 |
-Xms |
3670k |
-Xmx |
64m |
由於tenured generation的major collection較慢,所以tenured generation空間小於young generation的話,會造成頻繁的major collection,影響效率。Server JVM預設的young generation和tenured generation空間比例為1:2,也就是說young generation的eden和survivor空間之和是整個heap(當然不包括perm gen)的三分之一,該比例可以通過-XX:NewRatio=n參數來控制,而Client JVM預設的-XX:NewRatio是8。至於調整young generation空間大小的NewSize=n和MaxNewSize=n參數就不講了,請參考後面的資料。
young generation中倖存的對象被轉移到tenured generation,但不幸的是concurrent collector線程在這裡進行major collection,而在回收任務結束前空間被耗盡了,這時將會發生Full Collections(Full GC),整個應用程式都會停止下來直到回收完成。Full GC是高負載生產環境的噩夢……
現在來說說異類perm gen,它是JVM用來儲存無法在Java語言級描述的對象,這些對象分別是類和方法資料(與class loader有關)以及interned strings(字串駐留)。一般32位OS下perm gen預設64m,可通過參數-XX:MaxPermSize=n指定,JVM Memory Structure一文說,對於這塊地區,沒有更詳細的文獻了,神秘。
回到問題“為何會記憶體溢出。”。
要回答這個問題又要引出另外一個話題,既什麼樣的對象GC才會回收。當然是GC發現通過任何reference chain(引用鏈)無法訪問某個對象的時候,該對象即被回收。名詞GC Roots正是分析這一過程的起點,例如JVM自己確保了對象的可到達性(那麼JVM就是GC Roots),所以GC Roots就是這樣在記憶體中保持對象可到達性的,一旦不可到達,即被回收。通常GC Roots是一個在current thread(當前線程)的call stack(調用棧)上的對象(例如方法參數和局部變數),或者是線程自身或者是system class loader(系統類別載入器)載入的類以及native code(本地代碼)保留的使用中的物件。所以GC Roots是分析對象為何還存活於記憶體中的利器。知道了什麼樣的對象GC才會回收後,再來學習下對象引用都包含哪些吧。
從最強到最弱,不同的引用(可到達性)層級反映了對象的生命週期。
l Strong Ref(強引用):通常我們編寫的代碼都是Strong Ref,於此對應的是強可達性,只有去掉強可達,對象才被回收。
l Soft Ref(軟引用):對應軟可達性,只要有足夠的記憶體,就一直保持對象,直到發現記憶體吃緊且沒有Strong Ref時才回收對象。一般可用來實現緩衝,通過java.lang.ref.SoftReference類實現。
l Weak Ref(弱引用):比Soft Ref更弱,當發現不存在Strong Ref時,立刻回收對象而不必等到記憶體吃緊的時候。通過java.lang.ref.WeakReference和java.util.WeakHashMap類實現。
l Phantom Ref(虛引用):根本不會在記憶體中保持任何對象,你只能使用Phantom Ref本身。一般用於在進入finalize()方法後進行特殊的清理過程,通過java.lang.ref.PhantomReference實現。
有了上面的種種我相信很容易就能把heap和perm gen撐破了吧,是的利用Strong Ref,儲存大量資料,直到heap撐破;利用interned strings(或者class loader載入大量的類)把perm gen撐破。
關於shallow size、retained size
Shallow size就是對象本身佔用記憶體的大小,不包含對其他對象的引用,也就是對象頭加成員變數(不是成員變數的值)的總和。在32位系統上,對象頭佔用8位元組,int佔用4位元組,不管成員變數(對象或數組)是否引用了其他對象(執行個體)或者賦值為null它始終佔用4位元組。故此,對於String對象執行個體來說,它有三個int成員(3*4=12位元組)、一個char[]成員(1*4=4位元組)以及一個對象頭(8位元組),總共3*4 +1*4+8=24位元組。根據這一原則,對String a=”rosen jiang”來說,執行個體a的shallow size也是24位元組(很多人對此有爭議,請看官甄別並留言給我)。
Retained size是該對象自己的shallow size,加上從該對象能直接或間接訪問到對象的shallow size之和。換句話說,retained size是該對象被GC之後所能回收到記憶體的總和。為了更好的理解retained size,不妨看個例子。
把記憶體中的對象看成下圖中的節點,並且對象和對象之間互相引用。這裡有一個特殊的節點GC Roots,正解。這就是reference chain的起點。
從obj1入手,上圖中藍色節點代表僅僅只有通過obj1才能直接或間接訪問的對象。因為可以通過GC Roots訪問,所以左圖的obj3不是藍色節點;而在右圖卻是藍色,因為它已經被包含在retained集合內。
所以對於左圖,obj1的retained size是obj1、obj2、obj4的shallow size總和;右圖的retained size是obj1、obj2、obj3、obj4的shallow size總和。obj2的retained size可以通過相同的方式計算。
Heap Dump
heap dump是特定時間點,java進程的記憶體快照。有不同的格式來儲存這些資料,總的來說包含了快照被觸發時java對象和類在heap中的情況。由於快照只是一瞬間的事情,所以heap dump中無法包含一個對象在何時、何地(哪個方法中)被分配這樣的資訊。
在不同平台和不同java版本有不同的方式擷取heap dump,而MAT需要的是HPROF格式的heap dump二進位檔案。
如何擷取heap dump檔案。
通常來說,只要你設定了如下所示的 JVM 參數:
-XX:+HeapDumpOnOutOfMemoryError
JVM 就會在發生記憶體泄露時抓拍下當時的記憶體狀態,也就是我們想要的堆轉儲檔案。
如果你不想等到發生崩潰性的錯誤時才獲得堆轉儲檔案,也可以通過設定如下 JVM 參數來按需擷取堆轉儲檔案。
-XX:+HeapDumpOnCtrlBreak
除此之外,還有很多的工具,例如 JMap,JConsole 都可以協助我們得到一個堆轉儲檔案。使用jmap擷取heap dump的命令如下:
jmap -dump:format=b,file=<dumpfile> <pid>
解釋:format=b-->指定格式為二進位;file=<dumpfile>-->指定檔案名稱,自訂;<pid> -->進程id
由於我是windows+JDK5,所以選擇了-XX:-HeapDumpOnOutOfMemoryError這種方式,更多配置請參考MAT Wiki。
參考資料
MAT Wiki
Interned Strings
Strong,Soft,Weak,Phantom Reference
Tuning Garbage Collection with the 5.0 Java[tm] Virtual Machine
Permanent Generation
Understanding Weak References譯文
Java HotSpot VM Options
Shallow and retained sizes
JVM Memory Structure
GC roots