您好,登錄后才能下訂單哦!
推薦閱讀:我憑借這份pdf拿下了螞蟻金服、字節跳動、小米等大廠的offer
Java GC垃圾回收幾乎是面試必問的JVM問題之一,本篇文章帶領大家了解Java GC的底層原理,圖文并茂,突破學習及面試瓶頸。
JVM中堆的結構圖
圖中展示了堆中三個區域:Eden、From Survivor、To Survivor。從圖中可以也可以看到它們的大小比例,準確來說是:8:1:1。為什么要這樣設計呢,本篇文章后續會給出解答,還是根據垃圾回收的具體情況來設計的。
還記得在設置JVM時,常用的類似-Xms和-Xmx等參數嗎?對的它們就是用來說設置堆中各區域的大小的。
控制參數詳解:
對照上面兩個圖,再來看這些參數是不是沒有之前那么枯燥了,它們在圖中都有了對應的位置。
有沒有發現沒有直接設置老年代空間大小的參數?我們通過簡單的計算獲得。
老年代空間大小=堆空間大小-年輕代大空間大小
對上面參數立即了,但記憶有困難?那么,以下幾個助記詞可能更好的幫你記憶和理解參數的含義。
Xmx(memory maximum), Xms(memory startup), Xmn(memory nursery/new), Xss(stack size)。
對于參數的格式可以這樣理解:
垃圾收集(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都用到了哪些垃圾回收算法和分別應用在什么場景下的問題,再也不用死記硬背了吧?
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。