JVM記憶體管理
java不在需要開發人員顯示的分配記憶體和回收記憶體,而是由JVM自動管理記憶體的分配和回收(又稱為垃圾回收-GC),這簡化了程式設計難度,但同時可能使得程式設計師在不知不覺中浪費了很多記憶體,導致JVM花費很多時間進行垃圾回收。另外還有可能由於不清楚JVM的記憶體分配和回收機制造成記憶體洩露。最終導致JVM記憶體不夠用。
記憶體空間
在JVM規範中,將記憶體空間分為:方法區、堆、本地方法棧、PC暫存器、及JVM方法棧。如圖:
方法區
方法區主要儲存了類載入的資訊(名稱、修飾符等)、類的靜態變數、類中定義了為final型別的常量,類中的field資訊、類中的方法資訊。當開發人員在程式中通過Class物件的getName、isInstance等方法來獲取資訊時,這些資料都來源於方法區域。方法區域是全域性共享的。在一定條件下也會被GC。當方法區域要使用的記憶體大小超過其允許的大小是,會丟擲OutOfMemory異常。
在Sun JDK中,這塊區域對應Permanet Generation,又稱持久帶,預設最小值為16MB,最大值為64MB,可以通過
堆
堆用於儲存物件例項及陣列值,可以認為,java中所有通過new建立的物件的記憶體都在此分配。Heap中物件所佔用的記憶體由GC回收,在32位作業系統中最大為2GB,64為作業系統則沒有限制,其大小可以通過-Xms和-Xms來控制,-Xms為JVM啟動時申請的最小Heap記憶體,預設為實體記憶體的1/64但小於1GB,-Xmx為JVM可申請的最大Heap記憶體。預設為實體記憶體的1/4但小於1GB。預設當空餘堆記憶體小於40%時,JVM會增大Heap到-Xmx指定的大小。可通過-XX:MinHeapFreeRatio=”比例”來指定這個比例;當空餘堆記憶體大於70%時,JVM會減小到-Xms制定的大小,可以通過
-XX:MaxHeapFreeRatio=”比例”來指定這個值。對於執行系統而言。為了避免頻繁的調整Heap的大小。通常將-Xms和-Xmx大小設為一致。
為了讓記憶體回收更高效,SunJDK從1.2開始 對堆採用了分代管理。
New Generation
大多數情況下Java程式中新建的物件都是從新生代分配記憶體的。新生代由Eden Space和兩塊相同大小的Survivor Space(通常又稱為S0和S1或From和To)構成。可以通過-Xmm引數來指定新生代的大小。也可以通過-XX:SurvivorRatio來調整Eden Space及Survivor Space的大小。
Old Generation
用於存放新生代中經過多次垃圾回收仍然存在的物件,例如快取,新建的物件也有可能在舊生代上直接分配記憶體。主要有兩種情況(由不同的GC實現決定):一種為大物件,可以通過在啟動引數上設定-XX:PretenureSizeThreshold=1024(單位為位元組,預設值為0)設定當前物件超過多少為大物件。記憶體在舊生代中直接分配。次引數在新生代採用了Paraller Scavenge GC時無效。它會根據執行時狀況決定什麼時候在那個地方建立物件。
本地方法棧
本地方法棧用於支援native方法的執行。儲存了每個native 方法呼叫的狀態。在SunJDK的實現中,本地方法棧和JVM棧是同一個
PC暫存器和JVM方法棧
每個執行緒都會建立PC暫存器和JVM方法棧。PC暫存器佔用的可能為CPU暫存器或作業系統記憶體。JVM方法棧為執行緒私有。其在記憶體分配上非常高效。當方法執行完畢時,其對應的棧幀也會被釋放。
當JVM方法棧空間不足時,會丟擲StackOverflowError。在SunJDK中可以通過-Xss來指定其大小。
記憶體分配
Java物件所佔用的記憶體 主要是從堆中進行分配。堆是所有執行緒共享的。因此在堆上分配記憶體時需要加鎖,這導致了建立物件的開銷比較大。當堆空間不足時,會觸發GC。如果GC後空間仍然不足,則會丟擲OutOfMemory錯誤資訊。
SunJDK為了提升記憶體分配效率。會為每個新建立的執行緒在新生代的Eden Space上分配到一塊獨立的空間,這塊空間稱為TLAB,其大小由JVM根據執行情況計算而得。可以通過-XX:TLABWasteTargetPercent來設定TLAB可佔用的Eden Space的百分比。預設為1%。JVM根據這個比例、執行緒數量、執行緒是否頻繁建立物件來給每個執行緒分配合適大小的TLAB空間。在TLAB空間上分配記憶體時不用加鎖。因此JVM在給執行緒中的物件分配記憶體時,會盡量在TLAB上分配。如果物件過大,或者TLAB空間已用完,則仍然在堆上進行分配。因此在編寫Java程式時,通常多個小物件比大物件分配來的高效。可通過啟動引數:-XX:+PrintTLAB來檢視TLAB空間的使用情況。
記憶體回收
收集器
JVM通過GC來回收堆和方法區的記憶體。GC的基本原理會首先找到程式中不再被使用的物件,然後回收這些物件所佔用的記憶體。通過採用收集器的方式實現GC,主要的收集器有引用計數收集器和跟蹤收集器
引用計數收集器
引用計數收集器採用分散式的管理方式,通過計數器記錄物件是否被引用。當計數器為0時,說明此物件不再被使用,可以進行回收
上圖中,當ObjectA釋放對ObjectB的引用後,ObjectB的引用計數器值為0,此時可以回收ObjectB所佔用的記憶體。
引用計數器需要在每次物件賦值時進行計數器的增減,它有一定消耗。另外,引用計數器對於迴圈引用的場景沒有辦法實現回收,如果上圖中,ObjectA和ObjectB互相引用。那麼即使ObjectA釋放了對ObjectB、ObjectC的引用,也無法回收ObjectB、ObjectC。對於JAVA這種面向物件的對形成負責引用關係的語言來說,引用計數器作為GC不是很合適,SunJDK也未採用此方式。
跟蹤收集器
跟蹤收集器採用集中式的管理方式。全域性記錄資料的引用狀態。再基於一定條件觸發(例如定時、空間不足時),執行時需要從跟集合來掃描物件的引用關係。這可能會造成應用程式暫停。主要有複製、標記-清除、標記-壓縮三種實現演算法。
複製
複製採用的方式為從跟集合掃描出存活的物件,並將找到的存活物件複製到一塊新的完全未使用的空間中。
複製收集器方式僅需要從根集合掃描所有的存貨物件,當要回收的空間中存活的物件較少時,複製演算法會比較高效。其帶來的成本是要增加一塊空的記憶體空間及進行物件的移動
標記-清除
標記-清除採用的是從根集合開始掃描。對存活的物件進行標記。標記完畢後,再掃描整個空間未被標記的物件,並進行回收
標記-清除動作不需要對物件進行移動,且僅對其不存活的物件進行處理。在空間中存活的物件較多的情況下較為高效。但是此方式是直接回收不存活物件所佔用的記憶體 會造成記憶體碎片。
標記-壓縮
標記-壓縮採用標記-清除的方式對存活物件進行標記,但在清除時則不同,在回收不存活物件所佔用的記憶體空間後,會將其他所有的存活物件的左端空閒的空間進行移動,並跟新引用其物件的指標。
此方法成本比標記-清除高,好處是不產生記憶體碎片。
SunJDK中可用的GC
以上的三種跟蹤收集器各有優缺點,SunJDK根據執行的Java記憶體分析,認為程式中大部分物件的存活時間都是較短的,少部分物件是長期存活的。基於這個分析,SunJDK將JVM的堆分為了新生代和舊生代,並提供了不同的GC實現:
新生代可用GC
SunJDK認為新生代的物件通常存活時間較短,因此選擇了上述的複製演算法來實現物件的回收。根據上述複製演算法的介紹,在執行復制時,需要一塊未使用的記憶體空間來存放存活的物件。這是新生代又被劃分為Eden、S0和S1三塊空間的原因,Eden Space存放新建立的物件,S0和S1其中的一塊用於在Minor GC觸發時作為複製的目標空間。當其中一塊為複製的目標空間,另一塊中的內容則會被清空。因此通常由將S0、S1稱為From Space和To Space,SunJDK提供了序列GC、並行回收GC和並行GC三種方式來回收新生代物件所佔用的記憶體。對新生代物件所佔用記憶體進行回收通常又稱為Minor GC
1、序列GC(Serial GC)
當採用序列GC時,SurvivorRatio的值對應eden space/surivivor space,SurvivorRatio預設值為,例如放-Xmn設定為10M時,採用序列GC,eden space 的佔用空間為8M,兩個surivivor space各佔1M.新生代分配記憶體採用的為空閒指標的方式,指標保持最後一個分配的物件在新生代記憶體區間的位置。當有新的物件要分配記憶體時,只需要檢查剩餘的空間是否足夠存放新的物件,夠則更新指標並建立物件,不夠則出發Minor GC。
2、並行回收GC(Paraller Scavenge)
並行回收GC也是採用的複製演算法,但是在掃面和複製時都是採用多執行緒實現。並且並行回收GC對大的新生代回收做了很多優化,例如動態調整eden、s0、s1的大小,在多CPU上回收時間會比序列短。適合於多CPU、暫停時間要求短的應用上。並行回收是server級別(CPU核數超過2切實體記憶體超過2GB)上預設採用的回收方式。
3、並行GC(ParNew)
並行GC在基於SurvivorRadio的值劃分eden space和兩塊survivor space的方式上和序列GC一樣
並行GC和並行回收GC的區別是並行GC需配合舊生代使用CMS GC,CMS GC在進行舊生代GC時,有些過程是併發進行的。如果此時發生Minor GC,需要進行處理,而並行回收GC是沒有做這些處理的,也正是因為這些處理,ParNew和並行的舊生代GC無法同時使用。
舊生代和持久代可用GC
JDK提供了序列、並行、及併發三種GC來對舊生代和持久代佔用的記憶體進行回收
1、序列
序列時和新生代記憶體分配相同
序列基於標記-清除-壓縮實現,分為三個階段:
- 從根集合物件開始掃描,按照三色著色的方式對物件進行標記
- 遍歷整個舊生代,找出其中未標識的物件並進行回收
- 執行滑動壓縮,將存活的物件像舊生代空間的開始處進行滑動,最終留出一塊連續的到結尾處的空間
序列執行的整個過程需暫停應用,且採用單執行緒模式,消耗時間較長。
2、並行
並行採用標記-壓縮實現,記憶體分配和序列相同
其過程和序列相似,只是在多個執行緒上執行。
這種方式也需要耗費暫停時間,只是較上中方式可以縮短暫停時間。
3、併發(CMS:Cocurrent Mark-Sweep GC)
Mark-Sweep 方式要對整個空間的物件掃描並標記,這個過程會造成較長時間的應用暫停。有些應用對響應時間有很高的要求。因此,SunJDK提供了CMS GC,好處為GC的大部分動作均與應用併發進行.因此可以大大縮短應用暫停的時間.
CMS採用的是Mark-Sweep方式,其在回收完畢後可能會形成多個空閒空間,於是CMS採用free list的方式來記錄舊生代空間中哪些部分是空閒的。當有物件需要記憶體時,就先要去free list尋找有哪個部分是可以放下這個記憶體的。多數情況下舊生代分配記憶體的請求都來源於Minor GC階段,CMS這種分配舊生代記憶體的方式會導致Minor GC速度下降
另外,由於CMS執行過程中大部分時候適合應用併發進行的,分配記憶體的動作有可能和回收記憶體的動作同時進行,這是會造成free list激烈競爭,CMS為了避免這現象,引入了Mutual exclusion locks,以JVM分配記憶體為優先
CMS 執行掃面、著色和清除步驟如下:
1)、第一次標記(initial Marking)
該步驟需要暫停整個應用,掃描從根部集合物件到舊生代中可訪問的物件,並對這些物件進行著色。對於著色物件,CMS採用一個外部的bit陣列來進行
2)、併發標記
在初始化標記完成後,CMS恢復所有應用的執行緒,同時開始併發對之前著色過的物件進行輪詢,以標記這些物件為可訪問物件
3)、重新標記
該步驟會暫停整個應用,在2中應用可能會修改物件的引用關係或建立新物件因此要對這些改變或新建物件進行掃描,並重新著色
4)、併發收集
完成3後,恢復所有執行緒,這一步主要負責將沒有標記的物件回收。由於記憶體碎片,可能會造成每次分出去的記憶體比回收的小,為了避免這種現象,在進行Sweeping的時候,CMS會盡量將相鄰的空閒記憶體組成一塊。採用的方法是先從free list裡面刪去,然後重新加入
FULL GC
除了CMS GC外,在對舊生代和持久代觸發GC時,其實是對新生代、舊生代和持久代都進行GC,因此通常又稱為FULL GC。當Full GC 被觸發時,首先對新生代進行GC(在新生代採用的是PS GC時,可以通過-XX:-ScavgengeBeforeFullGC來禁止Full GC對新生代進行GC),然後對舊生
代GC、持久代GC。
除了直接呼叫System.gc外,觸發Full GC執行情況有下面四種:
1)、舊生代空間不足
2)、Permanet Genatation滿
Permanet Genatation存放一些class資訊等,當系統中要載入的類、反射的類、和呼叫的方法較多時Permanet Genatation可能會被佔滿
3)、CMS GC出現promotion failed和concurrent mode failure
4)、統計得到的Minor GC晉升到舊生代平局大學大於舊生代GC
Garbage First GC
除了以上所說的GC外,為了能控制某個片段內GC所佔用的最大暫停時間,例如100s裡面最多暫停1s,以滿足對響應時間有很高要求的應用,SunJDK 6 update 1以上的版本和JDK1.7中增加了一種Garbage First 的GC(G1),在此就不再詳述。
對JVM記憶體狀況的檢視有很多工具,諸如GC Portal /JConsole/JVisual VM/JMap/JHat/Jstat/Eclipse Memory Analyzer