JVM 垃圾收集演算法 標記-清楚、標記-複製、標記-整理
摘要
Java程式在執行過程中會產生大量的物件,但是記憶體大小是有限的,如果光用而不釋放,那記憶體遲早被耗盡。如C、C++程式,需要程式設計師手動釋放記憶體,Java則不需要,是由垃圾回收器去自動回收。
垃圾回收器回收記憶體至少需要做兩件事情:標記垃圾、回收垃圾。於是誕生了很多演算法及垃圾回收器。
垃圾判斷演算法
即判斷JVM中的所有物件,哪些物件是存活的,哪些物件可回收的演算法。
引用計數演算法
在物件中新增一個屬性用於標記物件被引用的次數,每多一個其他物件引用,計數+1,當引用失效時,計數-1,如果計數=0,表示沒有其他物件引用,就可以被回收。
這個演算法無法解決迴圈依賴的問題。
可達性分析演算法
通過一系列被稱為“GC Roots”的根物件作為起始節點集,從這些節點開始,根據引用關係鏈向下搜尋,如果某個物件無法被搜尋到,則說明該物件無引用執行,可回收。相反,則物件處於存活狀態,不可回收。
JVM中的實現是找到存活物件,未打標記的就是無用物件,GC時會回收。
哪些物件可以作為GC Root呢:
- 所有Java執行緒當前活躍的棧幀裡指向GC堆裡的物件的引用;換句話說,當前所有正在被呼叫的方法的引用型別的引數/區域性變數/臨時值。
- VM的一些靜態資料結構裡指向GC堆裡的物件的引用,例如說HotSpot VM裡的Universe裡有很多這樣的引用。
- JNI handles,包括global handles和local handles
- (看情況)所有當前被載入的Java類
- (看情況)Java類的引用型別靜態變數
- (看情況)Java類的執行時常量池裡的引用型別常量(String或Class型別)
- (看情況)String常量池(StringTable)裡的引用
垃圾回收演算法
1、標記-清除演算法
概念:
顧名思義,標記-清除演算法分為兩個階段,標記(mark)和清除(sweep)。
標記:遍歷所有的GC Roots,然後將所有的GC Roots可達的物件標記為存活的物件。
清除:清除的過程將遍歷所有堆中的物件,將沒有標記的物件全部清除。
圖解:
對上圖中的黃色部分進行垃圾回收,回收後的截圖如下所示:
從圖中可知,進行標記清理後,可用記憶體增加,但是清除垃圾後的記憶體地址不連線,出現垃圾碎片。
缺點:
1、執行效率不穩定,如果Java堆中包含大量物件,而且大部分是需要被回收的,這時必須記性大量標記及清除動作,導致標記和清除兩個過程執行效率都隨物件數量增長而降低。
2、記憶體空間碎片化的問題,標記、清除後會產生大量的不連續記憶體碎片,空間碎片太可能會導致當以後需要分配大物件時無法找到足夠的連續記憶體二不得不提前觸發另一次垃圾收集動作。
2、標記-複製演算法
概念:
複製演算法將記憶體分為兩個區間,這兩個區間是動態的,在任意一個時間點,所有分配的物件記憶體只能在其中一個區間(活動區間),另外一個區間就是空閒區間。
當有效記憶體空間耗盡時,JVM將暫停程式執行,開啟複製演算法GC執行緒。GC執行緒會將活動區間內的存活物件,全部複製到空閒區間,且嚴格按照記憶體地址一次排列,與此同時,GC執行緒將更新存活物件的記憶體引用地址指向新的記憶體地址。這個時候空閒記憶體已經變成了活動區間,垃圾物件全部在原來的活動區間,清理掉垃圾物件,原活動區間就變成了空閒區間。
這種方式記憶體的代價太高,每次基本上都要浪費一半的記憶體。於是將該演算法進行了改進,記憶體區域不再是按照1:1去劃分,而是將記憶體劃分為8:1:1三部分,較大那份記憶體是Eden區,其餘是兩塊較小的記憶體區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將物件複製到第二塊記憶體區上,然後清除Eden區,如果此時存活的物件太多,以至於Survivor不夠時,會將這些物件通過分配擔保機制複製到老年代中。(java堆又分為新生代和老年代)。
圖解:
優點:
1、很好地解決了“標記-清除”演算法,記憶體佈局混亂的缺點。
缺點:
1、浪費一半的記憶體。
2、假設物件存活率為100%,那麼“標記-複製”演算法的GC過程就是重複的把物件複製一遍,而且將所有的引用地址重置一遍。可以預見的複製所消耗的時間隨著物件存活率達到一定程度將會變成災難。所以“標記-複製”演算法使用的場景是可以忍受只是用50%記憶體,物件存活率非常低
3、標記-整理演算法
概念:
標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。
圖解:
優點:
1、彌補了“標記-清除”演算法,記憶體區域分散的缺點
2、彌補了“標記-複製”演算法記憶體減半的代價
缺點:
1、效率不高,對於“標記-清除”而言多了整理工作。
4、分代收集演算法
當前商業虛擬機器的垃圾收集都採用分代收集。此演算法沒啥新鮮的,就是將上述三種演算法整合了一下。具體如下:
根據各個年代的特點採取最適當的收集演算法
1、在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法。只需要付出少量存活物件的複製成本就可以完成收集。
2、老年代中因為物件存活率高、沒有額外空間對他進行分配擔保,就必須用標記-清除或者標記-整理。
測試案例
以下測試採用的是Serial加Serial Old收集器組合。
檢視當前jdk預設額收集器使用以下語句。
java -XX:+PrintCommandLineFlags -version
1、物件優先在Eden分配
測試程式碼