JVM(六)——GC 演算法

zhoupq發表於2019-02-14

JVM(六)——GC 演算法
標記-清除法


JVM(六)——GC 演算法

概述

  GC 是 JVM 自帶的功能,它能夠自動回收物件,清理記憶體,這是 Java 語言的一大優勢,但是GC絕不僅伴隨著Java,相反,GC歷史比Java更悠久。關於GC,我認為有四個問題需要解決:

  • 為什麼瞭解 GC?
  • 哪些記憶體需要回收?
  • 什麼時候回收?
  • 如何回收?

為什麼瞭解 GC

  GC 已經比較成熟,絕大部分情況下都“自動化”執行。之所以還需要了解GC,是因為當需要排查各種記憶體溢位、記憶體洩露問題時,當垃圾收整合為系統達到更高併發量的瓶頸時,我們就需要對這些“自動化”的技術實施必要的監控和調節。

哪些記憶體需要回收

  判定哪些記憶體需要回收,不是靠 JVM 去猜,也不是隨機,而是 JVM 靠一系列演算法得到結果,當然,演算法也是人寫的,雖然不能做到百分之百地符合所有開發者的要求,但這已經是最好的了。下面介紹一種判定記憶體回收的演算法——可達性分析演算法,這是我暫時能理解的演算法。

可達性分析演算法

JVM(六)——GC 演算法

  這個演算法的基本思路是通過一系列的稱為 “GC Roots” 的物件作為起點,從這些起點開始向下搜尋,搜尋走過的路徑稱為引用鏈,當一個物件到 GC Roots 沒有任何引用鏈相連時,則證明此物件是不可用的,也就是待回收的。例如,上圖中的 Object 5、Object 6 雖然兩者相互關聯,但是它們任何一個都和 GC Roots 不可達,所以 Object 5 和 Object 6 將會被判定為可回收的物件。

GC Roots

  至於什麼是 GC Roots 物件,解釋如下:

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

什麼時候回收

  當通過“可達性分析”等演算法標記好需要回收的物件後,等待它們的不是即可問斬,而是宣告“緩刑”。最後的回收條件是此物件是否有必要執行 finalize() 方法。

finalize() 方法

  當物件沒有覆蓋 finalize() 方法,或者 finalize() 方法已經被虛擬機器呼叫過,虛擬機器都將視作“沒有必要執行”。如果這個物件唄判定為有必要執行 finalize() 方法,那麼這個物件將會放置在一個叫作 F-Queue 的佇列中,這個佇列在後期會被某個JVM自動建立的執行緒執行,如果一個物件在finalize()方法中執行緩慢或者發生了死迴圈,將可能導致 F-Queue 佇列中其他物件永久處於等待狀態,甚至導致整個記憶體回收系統崩潰,幸運的是任何一個物件的 finalize() 方法只會被系統呼叫一次。
  在 《深入理解 JVM 虛擬機器》中,作者建議儘量避免使用 finalize() 方法,它不是c++ 中的解構函式,更像是java為適應c++程式設計師而作出的讓步。我覺得是finalize()方法是一根雞肋,一根如果運用不當將會引發災難的雞肋,如同 goto 語句。它的執行代價極高,不確定性大,無法保證各個物件的呼叫順序。它的功能經常會被誤以為類似 try-finally,然而 finalize() 能做的工作, try-finally 都能做,而且做得更好、更及時。   

如何回收

標記-清除法

  “標記-清除”(Mark-Sweep)法是最基礎的收集演算法。演算法分為兩個階段——“標記”和“清除”:首先標記出所有需要被回收的物件,在標記完成後統一回收所有標記了的物件,圖如篇首。
  它有兩個不足之處:

  • 效率不高
      標記和清除兩個過程的效率都不高
  • 產生大量不連續碎片
      雖然物件被回收,但是剩下的記憶體很有可能不連續,在這種情況下,當需要為系統分配一個較大物件時,會因為無法找到足夠的連續的記憶體而不得不提前觸發另一次垃圾收集動作。這樣,又會造成效率問題

  針對上述兩個缺點,後面兩種演算法對此進行了改進。

複製法

  “複製”(Copying)是為了解決“標記-清除”的效率不高問題而被髮明。它將記憶體按容量劃分為等量的兩塊。每次只使用其中的一塊。當使用的這一塊記憶體用完了,就將還存活的物件複製到另一塊(被稱作“保留區”)上面,然後再把已使用的記憶體空間一次清理掉。每次都對整個半區進行回收,再也不用擔心記憶體碎片化問題,圖如下所示。

JVM(六)——GC 演算法
複製法

  但是“複製”法也有缺點,每次都將記憶體縮小為一半,會導致記憶體利用率不高。並且在物件存活率高時進行復制操作,效率就會變低(因為存活的物件要複製到“保留區”)。當遇上極端情況,物件存活率百分百(以原記憶體一半為單位),那就需要另外的百分之五十的空間作為分配擔保。

標記-整理法

  “標記-整理”(Mark-Compact)法適合物件存活率高的情況使用。“標記”過程同“標記-清除”法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體,圖如下所示:

JVM(六)——GC 演算法
標記-整理法

分代收集法

  據知,當前商業虛擬機器都採用“分代收集”(Generational Collection)法,根據物件的存活週期的不同將記憶體劃分為幾塊。一般是把 Java 堆分成新生代和老年代,這樣可以根據更年代的特點採用最適當的收集演算法。

新生代

  在新生代中,每次垃圾收集時都會發現有大量物件死去,只有少量存活,那就選用複製演算法,只需要複製少量的物件就可以完成收集,成本小。

老年代

  老年代中的物件存活率高、沒有額外的空間對它進行分配擔保,就必須使用“標記-清除”、和“標記-整理”法來進行回收。

相關文章