深入理解JVM(六)-Java記憶體回收機制GC
對於C/C++開發人員來說,他們在記憶體管理方面具有至高的權利,但是也承擔著巨大的維護責任。而對於Java程式員來說,有了JVM(Java虛擬機器)管理機制的協助,再也不用擔心記憶體流失和記憶體溢出問題了。因此,這篇文章我將深入探討一下JVM,它的內部結構以及運行原理。
一、如何判斷對象已死
當GC要回收某個對象的時候,它是如何判斷該對象已死(即不可能再被使用),當一個對象不再被使用時,那麼這個對象就是可以被回收的。
(1)引用計數演算法
引用計數是垃圾收集器中的早期策略。在這種方法中,堆中每個對象(不是引用)都有一個引用計數。當一個對象被建立時,且將該對象分配給一個變數,該變數計數設定為1。當任何其它變數被賦值為這個對象的引用時,計數加1(a = b,則b引用的對象+1),但當一個對象的某個引用超過了生命週期或者被設定為一個新值時,對象的引用計數減1。任何引用計數為0的對象可以被當作垃圾收集。當一個對象被垃圾收集時,它引用的任何對象計數減1。
引用計數演算法實現簡單,很好理解,判斷效率也很高,大部分情況下是一個很不錯的演算法。但值得注意的是,主流的Java虛擬機器並沒有採用引用計數演算法,其主要的原因就是它很難解決對象之間的相互循環參考。很簡單的一個執行個體:
public class Main { public static void main(String[] args) { MyObject object1 = new MyObject(); MyObject object2 = new MyObject(); object1.object = object2; object2.object = object1; object1 = null; object2 = null; }}class MyObject{ public Object object = null;}
代碼中的對象object1與對象object2相互引用,這樣的情況在引用計數演算法下永遠都不會被回收,但實際情況下這樣的相互指引沒有任何實際意義。
(2)可達性分析演算法
通過一系列的稱為“GC Roots”的對象作為起點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈。當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是停用。
但注意,JVM中並不是判斷對象不可達就立即回收,被判定為不可達的對象要成為可回收對象必須至少經曆兩次標記過程,如果在這兩次標記過程中仍然沒有逃脫成為可回收對象的可能性,則基本上就真的成為可回收對象了。
藍色:仍然存活的對象
白色:判定可回收的對象<喎?http://www.bkjia.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxoMiBpZD0="二垃圾收集演算法">二、垃圾收集演算法
在確定了哪些垃圾可以被回收後,垃圾收集器要做的事情就是開始進行記憶體回收,但是這裡面涉及到一個問題是:如何高效地進行記憶體回收。由於Java虛擬機器規範並沒有對如何?垃圾收集器做出明確的規定,因此各個廠商的虛擬機器可以採用不同的方式來實現垃圾收集器,所以在此只討論幾種常見的垃圾收集演算法的核心思想。
(1)標記-清除演算法
最基礎的收集演算法是“標記-清除”(Mark-Sweep)演算法,如同它的名字一樣,演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收所有被標記的對象,它的標記過程其實在前一節講述對象標記判定時已經介紹過了。之所以說它是最基礎的收集演算法,是因為後續的收集演算法都是基於這種思路並對其不足進行改進而得到的。它的主要不足有兩個:一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之後會產生大量不連續的記憶體片段,空間片段太多可能會導致以後在程式運行過程中需要分配較大對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
(2)複製演算法
為瞭解決效率問題,一種稱為“複製”(Copying)的收集演算法出現了,它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的對象複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置時也就不用考慮記憶體片段等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,運行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半,未免太高了一點。現在的商業虛擬機器都採用這種收集演算法來回收新生代。
(3)標記-整理演算法
複製收集演算法在對象存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。
根據老年代的特點,有人提出了另外一種“標記-整理”(Mark-Compact)演算法,標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的記憶體。
(4)分代收集演算法
當前商業虛擬機器的垃圾收集都採用“分代收集”(Generational Collection)演算法,這種演算法並沒有什麼新的思想,只是根據對象存活周期的不同將記憶體劃分為幾塊。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製演算法,只需要付出少量存活對象的複製成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”演算法來進行回收。
三、垃圾收集器
如果說收集演算法是記憶體回收的方法論,垃圾收集器就是記憶體回收的具體實現。 Java虛擬機器規範中沒有規定垃圾收集器如何?,所以不同的廠商,不同版本的虛擬機器提供的垃圾收集器是有很大區別的。這裡討論的收集器基於Sun HotSpot虛擬機器1.6版 Update 22。
Serial收集器:新生代收集器,使用停止複製演算法,使用一個線程進行GC,其它背景工作執行緒暫停。使用-XX:+UseSerialGC可以使用Serial+Serial Old模式運行進行記憶體回收(這也是虛擬機器在Client模式下啟動並執行預設值)
ParNew收集器:新生代收集器,使用停止複製演算法,Serial收集器的多線程版,用多個線程進行GC,其它背景工作執行緒暫停,關注縮短垃圾收集時間。使用-XX:+UseParNewGC開關來控制使用ParNew+Serial Old收集器組合收集記憶體;使用-XX:ParallelGCThreads來設定執行記憶體回收的線程數。
Parallel Scavenge 收集器:新生代收集器,使用停止複製演算法,關注CPU輸送量,即運行使用者代碼的時間/總時間,比如:JVM運行100分鐘,其中運行使用者代碼99分鐘,垃 圾收集1分鐘,則輸送量是99%,這種收集器能最高效率的利用CPU,適合運行後台運算(關注縮短垃圾收集時間的收集器,如CMS,等待時間很少,所以適 合使用者互動,提高使用者體驗)。使用-XX:+UseParallelGC開關控制使用 Parallel Scavenge+Serial Old收集器組合回收垃圾(這也是在Server模式下的預設值);使用-XX:GCTimeRatio來設定使用者執行時間佔總時間的比例,預設99,即 1%的時間用來進行記憶體回收。使用-XX:MaxGCPauseMillis設定GC的最大停頓時間(這個參數只對Parallel Scavenge有效)
Serial Old收集器:老年代收集器,單線程收集器,使用標記整理(整理的方法是Sweep(清理)和Compact(壓縮),清理是將廢棄的對象幹掉,只留倖存 的對象,壓縮是將移動對象,將空間填滿保證記憶體分為2塊,一塊全是對象,一塊空閑)演算法,使用單線程進行GC,其它背景工作執行緒暫停(注意,在老年代中進行標 記整理演算法清理,也需要暫停其它線程),在JDK1.5之前,Serial Old收集器與ParallelScavenge搭配使用。
Parallel Old收集器:老年代收集器,多線程,多線程機制與Parallel Scavenge差不錯,使用標記整理(與Serial Old不同,這裡的整理是Summary(匯總)和Compact(壓縮),匯總的意思就是將倖存的對象複製到預先準備好的地區,而不是像Sweep(清 理)那樣清理廢棄的對象)演算法,在Parallel Old執行時,仍然需要暫停其它線程。Parallel Old在多核計算中很有用。Parallel Old出現後(JDK 1.6),與Parallel Scavenge配合有很好的效果,充分體現Parallel Scavenge收集器輸送量優先的效果。使用-XX:+UseParallelOldGC開關控制使用Parallel Scavenge +Parallel Old組合收集器進行收集。
CMS(Concurrent Mark Sweep)收集器:老年代收集器,致力於擷取最短回收停頓時間,使用標記清除演算法,多線程,優點是並發收集(使用者線程可以和GC線程同時工作),停頓小。使用-XX:+UseConcMarkSweepGC進行ParNew+CMS+Serial Old進行記憶體回收,優先使用ParNew+CMS(原因見後面),當使用者線程記憶體不足時,採用備用方案Serial Old收集。
CMS收集的方法是:先3次標記,再1次清除,3次標記中前兩次是初始標記和重新標記(此時仍然需要停止(stop the world)), 初始標記(Initial Remark)是標記GC Roots能關聯到的對象(即有引用的對象),停頓時間很短;並發標記(Concurrent remark)是執行GC Roots尋找引用的過程,不需要使用者線程停頓;重新標記(Remark)是在初始標記和並發標記期間,有標記變動的那部分仍需要標記,所以加上這一部分 標記的過程,停頓時間比並發標記小得多,但比初始標記稍長。在完成標記之後,就開始並發清除,不需要使用者線程停頓。
所以在CMS清理過程中,只有初始標記和重新標記需要短暫停頓,並發標記和並發清除都不需要暫停使用者線程,因此效率很高,很適合高互動的場合。
CMS也有缺點,它需要消耗額外的CPU和記憶體資源,在CPU和記憶體資源緊張,CPU較少時,會加重系統負擔(CMS預設啟動線程數為(CPU數量+3)/4)。
另外,在並發收集過程中,使用者線程仍然在運行,仍然產生記憶體垃圾,所以可能產生“浮動垃圾”,本次無法清理,只能下一次Full GC才清理,因此在GC期間,需要預留足夠的記憶體給使用者線程使用。所以使用CMS的收集器並不是老年代滿了才觸發Full GC,而是在使用了一大半(預設68%,即2/3,使用-XX:CMSInitiatingOccupancyFraction來設定)的時候就要進行Full GC,如果使用者線程消耗記憶體不是特別大,可以適當調高-XX:CMSInitiatingOccupancyFraction以降低GC次數,提高效能,如果預留的使用者線程記憶體不夠,則會觸發Concurrent Mode Failure,此時,將觸發備用方案:使用Serial Old 收集器進行收集,但這樣停頓時間就長了,因此-XX:CMSInitiatingOccupancyFraction不宜設的過大。
還有,CMS採用的是標記清除演算法,會導致記憶體片段的產生,可以使用-XX:+UseCMSCompactAtFullCollection來設定是否在Full GC之後進行磁碟重組,用-XX:CMSFullGCsBeforeCompaction來設定在執行多少次不壓縮的Full GC之後,來一次帶壓縮的Full GC。
G1收集器:在JDK1.7中正式發布,與現狀的新生代、老年代概念有很大不同,目前使用較少,不做介紹。四、記憶體配置以及回收策略Java虛擬機器結構
Java記憶體配置
Java堆中各代分布
(1) Young(年輕代):主要是用來存放新生的對象。對象被建立時,記憶體的分配首先發生在年輕代(大對象可以直接被建立在年老代),大部分的對象在建立後很快就不再使用,因此很快變得不可達,於是被年輕代的GC機制清理掉(IBM的研究表明,98%的對象都是很快消亡的),這個GC機制被稱為Minor GC或叫Young GC。注意,Minor GC並不代表年輕代記憶體不足,它事實上只表示在Eden區上的GC。
年輕代上的記憶體配置是這樣的,年輕代可以分為3個地區:Eden區(用來表示記憶體首次分配的地區)和兩個存活區(Survivor 0 、Survivor 1)。
絕大多數剛建立的對象會被分配在Eden區,其中的大多數對象很快就會消亡。Eden區是連續的記憶體空間,因此在其上分配記憶體極快;
當Eden區滿的時候,執行Minor GC,將消亡的對象清理掉,並將剩餘的對象複製到一個存活區Survivor0(此時,Survivor1 是空白的,兩個Survivor總有一個是空白的);
此後,每次Eden區滿了,就執行一次Minor GC,並將剩餘的對象都添加到Survivor0;
當Survivor0 也滿的時候,將其中仍然活著的對象直接複製到Survivor1,以後Eden區執行Minor GC後,就將剩餘的對象添加Survivor1(此時,Survivor0是空白的);
當兩個存活區切換了幾次(HotSpot虛擬機器預設15次,用-XX:MaxTenuringThreshold控制,大於該值進入老年代)之後,仍然存活的對象(其實只有一小部分,比如,我們自己定義的對象),將被複製到老年代。
(2)Old(年老代):主要存放應用程式中生命週期長的記憶體對象。
對象如果在年輕代存活了足夠長的時間而沒有被清理掉(即在幾次 Young GC 後存活了下來),則會被複製到年老代,年老代的空間一般比年輕代大,能存放更多的對象,在年老代上發生的GC次數也比年輕代少。當年老代記憶體不足時, 將執行Major GC,也叫 Full GC。
可以使用-XX:+UseAdaptiveSizePolicy開關來控制是否採用動態控制策略,如果動態控制,則動態調整Java堆中各個地區的大小以及進入老年代的年齡。
如果對象比較大(比如長字串或大數組),Young空間不足,則大對象會直接分配到老年代上(大對象可能觸發提前GC,應少用,更應避免使用短命的大對象)。用-XX:PretenureSizeThreshold 來控制直接升入老年代的對象大小,大於這個值的對象會直接分配在老年代上。
可能存在年老代對象引用新生代對象的情況,如果需要執行Young GC,則可能需要查詢整個老年代以確定是否可以清理回收,這顯然是低效的。解決的方法是,年老代中維護一個512 byte的塊——”card table“,所有老年代對象引用新生代對象的記錄都記錄在這裡。Young GC時,只要查這裡即可,不用再去查全部老年代,因此效能大大提高。
(3)Permanent(永久代):是指記憶體的永久儲存地區,也就是方法區,主要存放Class和Meta的資訊,Class在被 Load的時候被放入PermGen space地區. 它和和存放Instance的Heap地區不同,GC(Garbage Collection)不會在主程式運行期對PermGen space進行清理,所以如果你的APP會LOAD很多CLASS的話,就很可能出現PermGen space錯誤。
永久代的回收有兩種:常量池中的常量,無用的類資訊,常量的回收很簡單,沒有引用了就可以被回收。對於無用的類進行回收,必須保證3點:
類的所有執行個體都已經被回收; 載入類的ClassLoader已經被回收; 類對象的Class對象沒有被引用(即沒有通過反射引用該類的地方)。
永久代的回收並不是必須的,可以通過參數來設定是否對類進行回收。HotSpot提供-Xnoclassgc進行控制。
使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看類載入和卸載資訊-verbose、-XX:+TraceClassLoading可以在Product版HotSpot中使用;-XX:+TraceClassUnLoading需要fastdebug版HotSpot支援。
五、GC參數
堆設定
-Xms :初始堆大小
-Xmx :最大堆大小
-XX:NewSize=n :設定年輕代大小
-XX:NewRatio=n: 設定年輕代和年老代的比值。如:為3,表示年輕代與年老代比值為1:3,年輕代占整個年輕代年老代和的1/4
-XX:SurvivorRatio=n :年輕代中Eden區與兩個Survivor區的比值。注意Survivor區有兩個。如:3,表示Eden:Survivor=3:2,一個Survivor區占整個年輕代的1/5
-XX:MaxPermSize=n :設定持久代大小
收集器設定
-XX:+UseSerialGC :設定串列收集器
-XX:+UseParallelGC :設定並行收集器
-XX:+UseParalledlOldGC :設定並行年老代收集器
-XX:+UseConcMarkSweepGC :設定並發收集器
記憶體回收統計資訊
-XX:+PrintHeapAtGC GC的heap詳情
-XX:+PrintGCDetails GC詳情
-XX:+PrintGCTimeStamps 列印GC時間資訊
-XX:+PrintTenuringDistribution 列印年齡資訊等
-XX:+HandlePromotionFailure 老年代分配擔保(true or false)
並行收集器設定
-XX:ParallelGCThreads=n :設定並行收集器收集時使用的CPU數。並行收集線程數。
-XX:MaxGCPauseMillis=n :設定並行收集最大暫停時間
-XX:GCTimeRatio=n :設定記憶體回收時間占程式已耗用時間的百分比。公式為1/(1+n)
並發收集器設定
-XX:+CMSIncrementalMode :設定為增量模式。適用於單CPU情況。
-XX:ParallelGCThreads=n :設定並發收集器年輕代收集方式為並行收集時,使用的CPU數。並行收集線程數。