JVM基礎學習(二):記憶體分配策略與垃圾收集技術

Huangzzzzz發表於2022-02-21

Java與C++之間有一堵由記憶體動態分配和垃圾收集技術所圍成的高牆,牆外面的人想進去,牆裡面的人卻想出來

垃圾收集概述

Java記憶體模型中的堆和方法區是垃圾收集技術所需要關注的終點,因為其他的區域會跟隨執行緒的結束而自動回收。

而需要解決垃圾收集的首要目標便是解決如何判斷一個物件已經不需要了從而自動進行回收;判斷物件是否可以進行回收的演算法可以分為引用計數演算法可達性分析演算法

對於Redis有一些瞭解的同學應該知道Redis的物件記憶體回收演算法便是使用的引用計數演算法;而JVM都是使用的可達性分析演算法,在此我們只討論可達性分析演算法。

可達性分析演算法

可達性分析演算法簡而言之便是從一些稱為“GC Roots”的根物件作為起始節點集,根據引用關係向下搜尋,搜尋過的路徑稱為引用鏈,若某個物件到任何GC Roots物件都沒有引用鏈,那麼此物件便為不可達。

在Java技術體系中,固定作為GC Roots物件的包括以下幾種:

  • 虛擬機器棧中引用的變數(虛棧);
  • 方法區中類靜態屬性引用的物件(靜);
  • 方法區中常量引用的物件(常);
  • 本地方法棧中引用的變數(本棧);
  • JVM虛擬機器內部的引用(內);
  • 所有被同步鎖持有的物件(鎖);
  • 其他反映虛擬機器內部情況的物件;

垃圾收集演算法

從如何判定物件消亡的角度出發,垃圾收集演算法可以劃分為“引用計數式垃圾收集(直接垃圾收集)”與“追蹤式垃圾收集(間接垃圾收集)”。

分代收集理論

分帶收集理論是基於以下分代假說之上:

  • 弱分代假說:絕大多數物件都是朝生夕滅的;
  • 強分代假說:熬過越多次垃圾收集過程的物件就越難以消亡;
  • 跨代引用假說:跨代引用相對於同代引用來說僅佔極少數;

以上的假說共同奠定了垃圾收集器的一致的設計原則:收集器應該將Java堆劃分為不同的區域,然後根據回收物件的年齡分配到不同的區域中儲存。
所以現代JVM垃圾收集器一般將Java堆劃分為“新生代”與“老年代”;
針對不同分代的GC演算法有以下幾種:

  • 部分收集(Partial GC):不是完整收集整個Java堆的垃圾收集;
    • 新生代收集(Minor GC/Young GC):目標只是新生代的垃圾收集;
    • 老年代收集(Major GC/Old GC):目標只是老年代的垃圾收集,暫時只有CMS收集器實現了單獨的老年代收集;
    • 混合收集(Mixed GC):目標是收集整個新生代以及部分老年代的垃圾收集,暫時只有G1收集器有這種行為;
  • 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集;

跨代引用假說主要是解決跨越“新生代”與“老年代”的物件之間引用的問題,根據該假說所衍生出的對於跨代引用的解決方法是:

在“新生代”上建立一個全域性的資料結構(記憶集),該結構將老年代劃分為若干小塊,標識“老年代”中存在跨代引用的記憶體區域。此後發生Minor GC時,只有包含了跨代引用的小塊記憶體裡的物件才會假如GC Roots進行掃描。

垃圾收集演算法

垃圾收集演算法這裡介紹三種:

  • 標記-清除演算法
  • 標記-複製演算法
  • 標記-整理演算法

標記-清除演算法

標記-清除演算法是最早出現也是最基礎的演算法,其執行步驟為:

  1. 標記出需要回收的物件(標記);
  2. 統一回收掉所有已被標記的物件(清除);
  3. 或者第一步標記出存活的物件,第二步清理沒有被標記的物件;

標記-清除演算法的缺點如下:

  1. 執行效率不穩定,與需要被標記的物件數量相關;
  2. 記憶體碎片化問題,標記、清除之後會產生大量不連續的記憶體碎片,可能會導致後續如果需要分配較大物件時無法找到足夠的連續物件從而觸發另外一次GC;

標記-複製演算法

標記-複製演算法簡稱複製演算法,解決的是標記-清除演算法中面對大量可回收物件執行效率低的問題。
最早的標記-複製演算法是一種稱為“半區複製”的演算法,其將可用記憶體按照容量大小劃分為大小相等的兩塊,一塊用以平時使用,另外一塊用以GC時複製還存活的物件。
標記-複製演算法是現在的商用Java虛擬機器常用的演算法,但是“半區複製”演算法所浪費的記憶體過多達到了整個記憶體區域的一半,所以後續又進行了很多的改進;

Appel式回收

“Appel式回收”是一種更優化的半區複製分代策略,其將記憶體區域分為一塊較大的Eden空間與兩塊較小的Survivor空間,每次分配記憶體的時候只使用一塊Eden空間與一塊Survivor空間;
“Appel式回收”的具體做法是:

存放物件的時候只會使用Eden空間與一塊Survivor空間,然後垃圾收集的時候會將存活的物件移到另外一塊未被使用的Survivor區域。
其實就相當於是將存活物件一直有一個區域可以存放,這樣便避免了記憶體空間的浪費。
HotSpot虛擬機器的Serial、Parnew等新生代收集器均採取了這種策略設計新生代的記憶體佈局。

標記-整理演算法

標記-整理演算法是針對老年代物件的存亡特徵所提出的有針對性的演算法。標記步驟並沒有進行改變,整理步驟時,是將所有存活的物件都向記憶體物件一端移動,然後直接清理掉邊界以外的記憶體。
若移動存活物件,那麼便在物件移動過程中必須全程暫停使用者應用程式才能進行,被稱為“Stop The World”。
若不移動物件,那麼記憶體碎片化問題只能依賴更為複雜的記憶體分配器與記憶體訪問器解決。

總結

以上便是記憶體分配策略與垃圾收集技術的理論基礎,下一篇部落格介紹現代JVM的實現細節。

相關文章