面試官,不要再問我「Java GC垃圾回收機制」了
- 2019 年 10 月 20 日
- 筆記
Java GC垃圾回收幾乎是面試必問的JVM問題之一,本篇文章帶領大家了解Java GC的底層原理,圖文並茂,突破學習及面試瓶頸。
楔子-JVM內存結構補充
在上篇《JVM之內存結構詳解》中有些內容我們沒有講,本篇結合垃圾回收機制來一起學習。還記得JVM中堆的結構圖嗎?
圖中展示了堆中三個區域:Eden、From Survivor、To Survivor。從圖中可以也可以看到它們的大小比例,準確來說是:8:1:1。為什麼要這樣設計呢,本篇文章後續會給出解答,還是根據垃圾回收的具體情況來設計的。
還記得在設置JVM時,常用的類似-Xms和-Xmx等參數嗎?對的它們就是用來說設置堆中各區域的大小的。
(圖片來源於網絡)
控制參數詳解:
- -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沒有任何引用鏈相連,證明對象是不可用的。
上圖中中綠色部分為存活對象,灰色部分為可回收對象。雖然灰色部分內部依舊有關聯,但它們到GC Root是不可達的。
面試問題
面試官,說說Java GC都用了哪些算法?分別應用在什麼地方?
答:複製算法、標記清除、標記整理……
你還在單純的死記硬背么?繼續往下看,你會豁然開朗,再也不用死記硬背了。
標記清除算法
標記清除(Mark-Sweep)算法,包含「標記」和「清除」兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。
標記清除算法是最基礎的收集算法,後續的收集算法都是基於該思路並對其缺點進行改進而得到的。
主要缺點:一個是效率問題,標記和清除過程的效率都不高;另外是空間問題,標記清除之後會產生大量不連續的內存碎片,空間碎片太多可能會導致,當程序在以後的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
複製算法
複製(Copying)算法:將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當一塊內存用完了,就將還存活着的對象複製到另外一塊上,然後清理掉前一塊。
每次對半區內存回收時、內存分配時就不用考慮內存碎片等複雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
缺點:將內存縮小為一半,性價比低,持續複製長生存期的對象則導致效率低下。
JVM堆中新生代便採用複製算法。回到最初推分配結構圖。
在GC回收過程中,當Eden區滿時,還存活的對象會被複制到其中一個Survivor區;當回收時,會將Eden和使用的Survivor區還存活的對象,複製到另外一個Survivor區,然後對Eden和用過的Survivor區進行清理。
如果另外一個Survivor區沒有足夠的內存存儲時,則會進入老年代。
這裡針對哪些對象會進入老年代有這樣的機制:對象每經歷一次複製,年齡加1,達到晉陞年齡閾值後,轉移到老年代。
在這整個過程中,由於Eden中的對象屬於像浮萍一樣「瞬生瞬滅」的對象,所以並不需要1:1的比例來分配內存,而是採用了8:1:1的比例來分配。
而針對那些像「水熊蟲」一樣,歷經多次清理依舊存活的對象,則會進入老年代,而老年的清理算法則採用下面要講到的「標記整理算法」。
標記整理算法
標記整理(Mark-Compact)算法:標記過程與「標記-清除」算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然後直接清理掉端邊界以外的內存。
這種算法不既不用浪費50%的內存,也解決了複製算法在對象存活率較高時的效率低下問題。
分代收集算法
分代收集算法,基本思路:將Java的堆內存邏輯上分成兩塊,新生代和老年代,針對不同存活周期、不同大小的對象採取不同的垃圾回收策略。
而在新生代中大多數對象都是瞬間對象,只有少量對象存活,複製較少對象即可完成清理,因此採用複製算法。而針對老年代中的對象,存活率較高,又沒有額外的擔保內存,因此採用標記整理算法。
其實,回頭看,分代收集算法就是對新生代和老年代算法從策略維度的規劃而已。
小結
至此,當面試官再問Java GC都用到了哪些垃圾回收算法和分別應用在什麼場景下的問題,再也不用死記硬背了吧?
關於Java GC還有垃圾收集器及垃圾回收調優,我們將在後續文章中持續更新,歡迎關注公眾號「程序新視界」獲得第一手更新。
原文鏈接:《面試官,不要再問我「Java GC垃圾回收機制」了》
系列文章:《JVM之內存結構詳解》
參考資料:《深入理解java虛擬機》。