面試官,不要再問我“Java GC垃圾回收機制”了

二師兄-公眾號-程式新視界發表於2019-10-20

Java GC垃圾回收幾乎是面試必問的JVM問題之一,本篇文章帶領大家瞭解Java GC的底層原理,圖文並茂,突破學習及面試瓶頸。

楔子-JVM記憶體結構補充

在上篇《JVM之記憶體結構詳解》中有些內容我們沒有講,本篇結合垃圾回收機制來一起學習。還記得JVM中堆的結構圖嗎?

image

圖中展示了堆中三個區域:Eden、From Survivor、To Survivor。從圖中可以也可以看到它們的大小比例,準確來說是:8:1:1。為什麼要這樣設計呢,本篇文章後續會給出解答,還是根據垃圾回收的具體情況來設計的。

還記得在設定JVM時,常用的類似-Xms和-Xmx等引數嗎?對的它們就是用來說設定堆中各區域的大小的。

image
(圖片來源於網路)

控制引數詳解:

  • -Xms設定堆的最小空間大小。
  • -Xmx設定堆的最大空間大小。
  • -Xmn堆中新生代初始及最大大小(NewSize和MaxNewSize為其細化)。
  • -XX:NewSize設定新生代最小空間大小。
  • -XX:MaxNewSize設定新生代最大空間大小。
  • -XX:PermSize設定永久代最小空間大小。
  • -XX:MaxPermSize設定永久代最大空間大小。
  • -Xss設定每個執行緒的堆疊大小。

對照上面兩個圖,再來看這些引數是不是沒有之前那麼枯燥了,它們在圖中都有了對應的位置。

有沒有發現沒有直接設定老年代空間大小的引數?我們通過簡單的計算獲得。

老年代空間大小=堆空間大小-年輕代大空間大小

對上面引數立即了,但記憶有困難?那麼,以下幾個助記詞可能更好的幫你記憶和理解引數的含義。

Xmx(memory maximum), Xms(memory startup), Xmn(memory nursery/new), Xss(stack size)。

對於引數的格式可以這樣理解:

  • -: 標準VM選項,VM規範的選項。
  • -X: 非標準VM選項,不保證所有VM支援。
  • -XX: 高階選項,高階特性,但屬於不穩定的選項。

GC概述

垃圾收集(Garbage Collection)通常被稱為“GC”,由虛擬機器“自動化”完成垃圾回收工作。

思考一個問題,既然GC會自動回收,開發人員為什麼要學習GC和記憶體分配呢?為了能夠配置上面的引數配置?引數配置又是為了什麼?

當需要排查各種記憶體溢位,記憶體洩露問題時,當垃圾成為系統達到更高併發量的瓶頸時,我們就需要對GC的自動回收實施必要的監控和調節。

JVM中程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生隨執行緒而滅。棧幀隨著方法的進入和退出做入棧和出棧操作,實現了自動的記憶體清理。它們的記憶體分配和回收都具有確定性。

因此,GC垃圾回收主要集中在堆和方法區,在程式執行期間,這部分記憶體的分配和使用都是動態的。

下面通過概念和具體的演算法來了解GC垃圾回收的過程。

如何判斷物件存活

判斷物件常規有兩種方法:引用計數演算法和可達性分析演算法(Reachability Analysis)。

引用計數演算法:給物件新增一個引用計數器,每當有一個地方引用它時計數器加1,引用釋放時計數減1,當計數器為0時可以回收。

引用計數演算法實現簡單,判斷高效,在微軟COM和Python語言等被廣泛使用,但在主流的Java虛擬機器中沒有使用該方法,主要是因為無法解決物件相互迴圈引用的問題。

可達性分析演算法:基本思想是通過一系列稱為“GC Root”的物件(如系統類載入器、棧中的物件、處於啟用狀態的執行緒等)作為起點,基於物件引用關係,開始向下搜尋,所走過的路徑稱為引用鏈,當一個物件到GC Root沒有任何引用鏈相連,證明物件是不可用的。

image

上圖中中綠色部分為存活物件,灰色部分為可回收物件。雖然灰色部分內部依舊有關聯,但它們到GC Root是不可達的。

面試問題

面試官,說說Java GC都用了哪些演算法?分別應用在什麼地方?

答:複製演算法、標記清除、標記整理……

你還在單純的死記硬背麼?繼續往下看,你會豁然開朗,再也不用死記硬背了。

標記清除演算法

標記清除(Mark-Sweep)演算法,包含“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後統一回收掉所有被標記的物件。

標記清除演算法是最基礎的收集演算法,後續的收集演算法都是基於該思路並對其缺點進行改進而得到的。

image

主要缺點:一個是效率問題,標記和清除過程的效率都不高;另外是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致,當程式在以後的執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

複製演算法

複製(Copying)演算法:將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當一塊記憶體用完了,就將還存活著的物件複製到另外一塊上,然後清理掉前一塊。

image

每次對半區記憶體回收時、記憶體分配時就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。

缺點:將記憶體縮小為一半,價效比低,持續複製長生存期的物件則導致效率低下。

JVM堆中新生代便採用複製演算法。回到最初推分配結構圖。

image

在GC回收過程中,當Eden區滿時,還存活的物件會被複制到其中一個Survivor區;當回收時,會將Eden和使用的Survivor區還存活的物件,複製到另外一個Survivor區,然後對Eden和用過的Survivor區進行清理。

如果另外一個Survivor區沒有足夠的記憶體儲存時,則會進入老年代。

這裡針對哪些物件會進入老年代有這樣的機制:物件每經歷一次複製,年齡加1,達到晉升年齡閾值後,轉移到老年代。

在這整個過程中,由於Eden中的物件屬於像浮萍一樣“瞬生瞬滅”的物件,所以並不需要1:1的比例來分配記憶體,而是採用了8:1:1的比例來分配。

而針對那些像“水熊蟲”一樣,歷經多次清理依舊存活的物件,則會進入老年代,而老年的清理演算法則採用下面要講到的“標記整理演算法”。

標記整理演算法

標記整理(Mark-Compact)演算法:標記過程與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體。

image

這種演算法不既不用浪費50%的記憶體,也解決了複製演算法在物件存活率較高時的效率低下問題。

分代收集演算法

分代收集演算法,基本思路:將Java的堆記憶體邏輯上分成兩塊,新生代和老年代,針對不同存活週期、不同大小的物件採取不同的垃圾回收策略。

而在新生代中大多數物件都是瞬間物件,只有少量物件存活,複製較少物件即可完成清理,因此採用複製演算法。而針對老年代中的物件,存活率較高,又沒有額外的擔保記憶體,因此採用標記整理演算法。

其實,回頭看,分代收集演算法就是對新生代和老年代演算法從策略維度的規劃而已。

小結

至此,當面試官再問Java GC都用到了哪些垃圾回收演算法和分別應用在什麼場景下的問題,再也不用死記硬背了吧?

關於Java GC還有垃圾收集器及垃圾回收調優,我們將在後續文章中持續更新,歡迎關注公眾號“程式新視界”獲得第一手更新。

原文連結:《面試官,不要再問我“Java GC垃圾回收機制”了
系列文章:《JVM之記憶體結構詳解

參考資料:《深入理解java虛擬機器》。


程式新視界

關注程式設計師的職場生涯,大量優質學習資源、技術文章分享

程式新視界-微信公眾號

相關文章