老生常談Java虛擬機器垃圾回收機制(必看篇)

lihong發表於2019-05-12

二、垃圾收集

垃圾收集主要是針對堆和方法區進行。

程式計數器、虛擬機器棧和本地方法棧這三個區域屬於執行緒私有的,只存在於執行緒的生命週期內,執行緒結束之後也會消失,因此不需要對這三個區域進行垃圾回收。

 

判斷一個物件是否可被回收

1. 引用計數演算法

給物件新增一個引用計數器,當物件增加一個引用時計數器加 1,引用失效時計數器減 1。引用計數為 0 的物件可被回收。

兩個物件出現迴圈引用的情況下,此時引用計數器永遠不為 0,導致無法對它們進行回收。

正因為迴圈引用的存在,因此 Java 虛擬機器不使用引用計數演算法。

public class ReferenceCountingGC {
    public Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGC objectA = new ReferenceCountingGC();
        ReferenceCountingGC objectB = new ReferenceCountingGC();
        objectA.instance = objectB;
        objectB.instance = objectA;
    }
}

2. 可達性分析演算法

透過 GC Roots 作為起始點進行搜尋,能夠到達到的物件都是存活的,不可達的物件可被回收。

Java 虛擬機器使用該演算法來判斷物件是否可被回收,在 Java 中 GC Roots 一般包含以下內容:

  • 虛擬機器棧中區域性變數表中引用的物件
  • 本地方法棧中 JNI 中引用的物件
  • 方法區中類靜態屬性引用的物件
  • 方法區中的常量引用的物件

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

3. 方法區的回收

因為方法區主要存放永久代物件,而永久代物件的回收率比新生代低很多,因此在方法區上進行回收價效比不高。

主要是 對常量池的回收 對類的解除安裝

在大量使用反射、動態代理、CGLib 等 ByteCode 框架、 動態生成 JSP 以及 OSGi 這類頻繁自定義 ClassLoader 的場景都需要虛擬機器具備類解除安裝功能,以保證不會出現記憶體溢位。

類的解除安裝條件很多,需要滿足以下三個條件,並且滿足了也不一定會被解除安裝:

  • 該類所有的例項都已經被回收,也就是 堆中不存在該類的任何例項
  • 載入該類的  ClassLoader 已經被回收
  • 該類對應的  Class 物件沒有在任何地方被引用 ,也就無法在任何地方透過反射訪問該類方法。

可以透過 -Xnoclassgc 引數來控制是否對類進行解除安裝。

4. finalize()

finalize() 類似 C++ 的解構函式,用來做關閉外部資源等工作。但是 try-finally 等方式可以做的更好, 並且該方法執行代價高昂,不確定性大,無法保證各個物件的呼叫順序,因此最好不要使用。

當一個物件可被回收時,如果需要執行該物件的 finalize() 方法, 那麼就有可能在該方法中讓物件重新被引用,從而實現自救。  自救只能進行一次 ,如果回收的物件之前呼叫了 finalize() 方法自救,後面回收時不會呼叫 finalize() 方法。

引用型別

無論是透過引用計算演算法判斷物件的引用數量,還是透過可達性分析演算法判斷物件是否可達, 判定物件是否可被回收都與引用有關。

Java 提供了四種強度不同的引用型別。

1. 強引用

被強引用關聯的物件 不會被回收

使用 new 一個新物件的方式來建立強引用。

Object obj = new Object();

2. 軟引用

被軟引用關聯的物件只有 在記憶體不夠的情況下才會被回收

使用 SoftReference 類來建立軟引用。

Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;  // 使物件只被軟引用關聯

3. 弱引用

被弱引用關聯的物件 一定會被回收 ,也就是說它只能存活到下一次垃圾回收發生之前。

使用 WeakReference 類來實現弱引用。

Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;

4. 虛引用

又稱為幽靈引用或者幻影引用。一個物件是否有虛引用的存在, 完全 不會對其生存時間構成影響 ,也無法透過虛引用取得一個物件。

為一個物件設定虛引用關聯的唯一目的就是能 在這個物件被回收時收到一個系統通知

使用 PhantomReference 來實現虛引用。

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;

垃圾收集演算法

1. 標記 - 清除

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

將存活的物件進行標記,然後清理掉未被標記的物件。

不足:

  • 標記和清除過程效率都不高;
  • 會產生大量不連續的記憶體碎片,導致無法給大物件分配記憶體。

2. 標記 - 整理

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

3. 複製

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

將記憶體劃分為大小相等的兩塊,每次只使用其中一塊,當這一塊記憶體用完了就將還存活的物件複製到另一塊上面,然後再把使用過的記憶體空間進行一次清理。

主要不足是隻使用了記憶體的一半。

現在的商業虛擬機器都採用這種收集演算法來回收新生代,但是並不是將新生代劃分為大小相等的兩塊,而是分為一塊較大的 Eden 空間和兩塊較小的 Survivor 空間,每次使用 Eden 空間和其中一塊 Survivor。在回收時,將 Eden 和 Survivor 中還存活著的物件一次性複製到另一塊 Survivor 空間上,最後清理 Eden 和使用過的那一塊 Survivor。

HotSpot 虛擬機器的 Eden 和 Survivor 的大小比例預設為 8:1,保證了記憶體的利用率達到 90%。如果每次回收有多於 10% 的物件存活,那麼一塊 Survivor 空間就不夠用了,此時需要依賴於老年代進行分配擔保,也就是借用老年代的空間儲存放不下的物件。

4. 分代收集

現在的商業虛擬機器採用分代收集演算法,它根據物件存活週期將記憶體劃分為幾塊,不同塊採用適當的收集演算法。

一般將堆分為新生代和老年代。

  • 新生代使用:複製演算法
  • 老年代使用:標記 - 清除 或者 標記 - 整理 演算法

垃圾收集器

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

以上是 HotSpot 虛擬機器中的 7 個垃圾收集器,連線表示垃圾收集器可以配合使用。

  • 單執行緒與多執行緒:單執行緒指的是垃圾收集器只使用一個執行緒進行收集,而多執行緒使用多個執行緒;
  • 序列與並行:序列指的是垃圾收集器與使用者程式交替執行,這意味著在執行垃圾收集的時候需要停頓使用者程式;並行指的是垃圾收集器和使用者程式同時執行。除了 CMS  和  G1 之外,其它垃圾收集器都是以序列的方式執行。

1. Serial 收集器

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

Serial 翻譯為序列,也就是說它以序列的方式執行。

它是單執行緒的收集器,只會使用一個執行緒進行垃圾收集工作。

它的優點是簡單高效,對於單個 CPU 環境來說,由於 沒有執行緒互動的開銷 ,因此擁有最高的單執行緒收集效率。

它是 Client 模式下的 預設新生代收集器 ,因為在該應用場景下,分配給虛擬機器管理的記憶體一般來說不會很大。Serial 收集器收集幾十兆甚至一兩百兆的新生代停頓時間可以控制在一百多毫秒以內,只要不是太頻繁,這點停頓是可以接受的。

2. ParNew 收集器

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

它是 Serial 收集器的多執行緒版本。

是 Server 模式下的虛擬機器首選新生代收集器,除了效能原因外,主要是因為除了 Serial 收集器,只有它能與 CMS 收集器配合工作。

預設開啟的執行緒數量與 CPU 數量相同,可以使用 -XX:ParallelGCThreads 引數來設定執行緒數。

3. Parallel Scavenge 收集器

與 ParNew 一樣是多執行緒收集器。

其它收集器關注點是儘可能縮短垃圾收集時使用者執行緒的停頓時間,而它的目標是達到一個可控制的吞吐量,它被稱為“吞吐量優先”收集器。這裡的吞吐量指 CPU 用於執行使用者程式碼的時間佔總時間的比值。

停頓時間越短就越適合需要與使用者互動的程式,良好的響應速度能提升使用者體驗。而高吞吐量則可以高效率地利用 CPU 時間,儘快完成程式的運算任務,適合在後臺運算而不需要太多互動的任務。

縮短停頓時間是以犧牲吞吐量和新生代空間來換取的:新生代空間變小,垃圾回收變得頻繁,導致吞吐量下降。

可以透過一個開關引數開啟 GC 自適應的調節策略(GC Ergonomics), 就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 區的比例、晉升老年代物件年齡等細節引數了。 虛擬機器會根據當前系統的執行情況收集效能監控資訊, 動態調整這些引數以提供最合適的停頓時間或者最大的吞吐量。 自適應調節策略是 Parallel Scavenge 收集器和 ParNew 收集器的一個重要區別。

4. Serial Old 收集器

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

是 Serial 收集器的老年代版本,也是給 Client 模式下的虛擬機器使用,採用標記-整理演算法。如果用在 Server 模式下,它有兩大用途:

  • 在 JDK 1.5 以及之前版本(Parallel Old 誕生以前)中與 Parallel Scavenge 收集器搭配使用。
  • 作為 CMS 收集器的後備預案,在併發收集發生 Concurrent Mode Failure 時使用。

5. Parallel Old 收集器

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

是 Parallel Scavenge 收集器的老年代版本,採用標記-整理演算法。

在注重吞吐量以及 CPU 資源敏感的場合,都可以優先考慮 Parallel Scavenge 加 Parallel Old 收集器。

6. CMS 收集器

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

CMS(Concurrent Mark Sweep), Mark Sweep 指的是標記 - 清除演算法

分為以下四個流程:

  • 初始標記:僅僅只是標記一下 GC Roots 能直接關聯到的物件,速度很快,需要停頓。
  • 併發標記:進行 GC Roots Tracing 的過程,它在整個回收過程中耗時最長,不需要停頓。
  • 重新標記:為了修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄,需要停頓。
  • 併發清除:不需要停頓。

在整個過程中耗時最長的併發標記和併發清除過程中,收集器執行緒都可以與使用者執行緒一起工作,不需要進行停頓,具有 併發收集、低停頓 的優點。

具有以下缺點:

  • 吞吐量低:低停頓時間是以犧牲吞吐量為代價的,導致  CPU 利用率不夠高
  • 無法處理浮動垃圾 ,可能出現 Concurrent Mode Failure。 浮動垃圾是指併發清除階段由於使用者執行緒繼續執行而產生的垃圾,這部分垃圾只能到下一次 GC 時才能進行回收 。 由於浮動垃圾的存在,因此需要預留出一部分記憶體,意味著 CMS 收集不能像其它收集器那樣等待老年代快滿的時候再回收。如果預留的記憶體不夠存放浮動垃圾,就會出現 Concurrent Mode Failure,這時虛擬機器將臨時啟用 Serial Old 來替代 CMS。
  • 標記 - 清除演算法導致的 空間碎片 ,往往出現老年代空間剩餘,但無法找到足夠大連續空間來分配當前物件,不得不提前觸發一次 Full GC。

CMS 已經在 JDK 9 中被標記為廢棄( deprecated )。

7. G1 收集器

G1(Garbage-First),它是一款面向服務端應用的垃圾收集器,在多 CPU 和大記憶體的場景下有很好的效能。HotSpot 開發團隊賦予它的使命是未來可以替換掉 CMS 收集器。

堆被分為新生代和老年代,其它收集器進行收集的範圍都是整個新生代或者老年代,而  G1 可以直接對新生代和老年代一起回收

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

G1 把堆劃分成多個大小相等的獨立區域(Region),Region的大小是一致的,數值是在1M到32M位元組之間的一個2的冪值數,JVM會盡量劃分2048個左右、同等大小的Region, 新生代和老年代不再物理隔離

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

透過引入 Region 的概念,從而將原來的一整塊記憶體空間劃分成多個的小空間,使得每個小空間可以單獨進行垃圾回收。這種劃分方法帶來了很大的靈活性,使得可預測的停頓時間模型成為可能。透過記錄每個 Region 垃圾回收時間以及回收所獲得的空間(這兩個值是透過過去回收的經驗獲得),並維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的 Region。

每個 Region 都有一個  Remembered Set ,用來記錄該 Region 物件的引用物件所在的 Region。透過使用 Remembered Set,在做可達性分析的時候就可以避免全堆掃描。

老生常談Java虛擬機器垃圾回收機制(必看篇)

 

如果不計算維護 Remembered Set 的操作,G1 收集器的運作大致可劃分為以下幾個步驟:

  • 初始標記
  • 併發標記
  • 最終標記:為了修正在併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機器將這段時間物件變化記錄線上程的 Remembered Set Logs 裡面,最終標記階段需要把 Remembered Set Logs 的資料合併到 Remembered Set 中。這階段需要停頓執行緒,但是可並行執行。
  • 篩選回收:首先對各個 Region 中的回收價值和成本進行排序,根據使用者所期望的 GC 停頓時間來制定回收計劃。此階段其實也可以做到與使用者程式一起併發執行,但是因為只回收一部分 Region,時間是使用者可控制的,而且停頓使用者執行緒將大幅度提高收集效率。

從 GC 演算法的角度, G1 選擇的是複合演算法,可以簡化理解為:

  • 在新生代,G1採用的仍然是 並行的複製演算法 ,所以同樣會發生Stop-The-World的暫停。
  • 在老年代,大部分情況下都是併發標記,而整理(Compact)則是和新生代GC時捎帶進行,並且不是整體性的整理,而是 增量 進行的。

具備如下特點:

  • 空間整合: 整體來看是基於“標記 - 整理”演算法 實現的收集器,從區域性(兩個 Region 之間)上來看是基於“複製”演算法實現的,這意味著執行期間不會產生記憶體空間碎片。
  • 可預測的停頓:能讓使用者明確指定在一個長度為 M 毫秒的時間片段內,消耗在 GC 上的時間不得超過 N 毫秒。

目前尚處於開發中的 JDK 11, JDK 又增加了兩種全新的 GC 方式,分別是:

  • Epsilon GC,簡單說就是個不做垃圾收集的GC,似乎有點奇怪,有的情況下,例如在進行效能測試的時候,可能需要明確判斷GC本身產生了多大的開銷,這就是其典型應用場景。
  • ZGC,這是Oracle開源出來的一個超級GC實現,具備令人驚訝的擴充套件能力,比如支援T bytes級別的堆大小,並且保證絕大部分情況下,延遲都不會超過10 ms。雖然目前還處於實驗階段,僅支援 Linux 64 位的平臺,但其已經表現出的能力和潛力都非常令人期待。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69917606/viewspace-2644063/,如需轉載,請註明出處,否則將追究法律責任。

相關文章