JAVA GC垃圾回收機制詳解

引言

在上一篇博客中我們詳細分析了,JVM的內存結構。既然提到了JVM那麼我們必須得談一談GC垃圾回收機制了,在分析垃圾回收機制之前呢,我們還是需要來內存結構中堆(Heap)的詳細結構。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設置每個線程的堆棧大小。

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

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

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

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

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回收流程

                             

(1)Eden區域是用來存放使用new或者newInstance等方式創建的對象,默認都是存放在Eden區,除非這個對象太大,或者超過了設定的閾值-XX:PretenureSizeThresold,這樣的對象會被直接分配到Old區域。

(2)2個Survivor(倖存)區,一般稱S0,S1,理論上他們是一樣大的,解釋一下,他們是如何工作的:

在不斷創建對象的過程中,Eden區會滿,這時候會開始做Young G也叫Minor GC,而Young空間的第一次GC就是找出Eden區中,倖存活着的對象,然後將這些對象,放到S0,或S1區中的其中一個, 假設第一次選擇了S0,它會逐步將活着的對象拷貝到S0區域,但是如果S0區域滿了,剩下活着的對象只能放old區域了,接下來要做的是,將Eden區域 清空,此時時候S1區域也是空的。

當第二次Eden區域滿的時候,就將Eden區域中活着的對象+S0區域中活着的對象,遷移到S1中,如果S1放不下,就會將剩下的部門,放到Old區域中,只是這次對象來源區域增加了S0,最後會將Eden區+S0區域,清空

第三次和第四次依次類推,始終保證S0和S1有一個是空的,用來存儲臨時對象,用於交換空間的目的,反反覆覆多次沒有被淘汰的對象,將會放入old區域中,默認是15次。具體的交換過程就和上圖中的信息相似。

 

如何判斷對象存活

判斷對象常規有兩種方法:引用計數算法和可達性分析算法(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的堆內存邏輯上分成兩塊,新生代和老年代,針對不同存活週期、不同大小的對象採取不同的垃圾回收策略。

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

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

常見問題錦集

1、對象進入Old區域有什麼壞處?

old區域一般稱爲老年代,老年代與新生代不一樣,年輕代,我們可以認爲存活下來的對象很少,而老年代則相反,存活下來的對象很多,所以JVM的 堆內存,纔是我們通常關注的主戰場,因爲這裏面活着的對象非常多,所以發生一次FULL GC,來找出來所有存活的對象是非常耗時的,因此,我們應該儘量避免FULL GC的發生。

2、S0和S1一般多大,靠什麼參數來控制,有什麼變化?

一般來說很小,我們大概知道它與Young差不多相差一倍的比例,設置的的參數主要有兩個:

-XX:SurvivorRatio=8

-XX:InitialSurvivorRatio=8

第一個參數是Eden和Survivor區域比重,注意是一個Survivor的的大小,如果將其設置爲8,則說明Eden區是一個Survivor區的8倍,換句話說S0或S1空間是整個Young空間的1/10,剩餘的80%由Eden區域來使用。

第二個參數是Young/S0的比值,當其設置爲8時,表示s0或s1佔整個Young空間的12.5%。

3、一個對象每次Minor Gc時,活着的對象都會在s0和s1區域轉移,經過經過Minor GC多少次後,會進入Old區域呢?

默認是15次,參數設置-XX:MaxTenuringThreshold=15,計數器會在對象的頭部記錄它交換的次數

4、爲什麼發生FULL GC會帶來很大的危害?

在發生FULL GC的時候,意味着JVM會安全的暫停所有正在執行的線程(Stop The World),來回收內存空間,在這個時間段內,所有除了回收垃圾的線程外,其他有關JAVA的程序,代碼都會靜止,反映到系統上,就會出現系統響應大幅度變慢,卡機等狀態。

舉個通俗易懂點的例子,就是在一個房間裏,如果有一個人,不停的扔垃圾,然後有一個清潔工不停掃垃圾,這時候,我們的系統是OK的,因爲基本不會 出現垃圾堆滿房間的情景,而且因爲清潔工可以對付過來,假設現在有10個人不停扔垃圾,那麼就房間就會很快被堆滿,這時候清潔工,由於工作不過來了,大聲 吼一聲,你們都暫停3分鐘,別再扔了,我先把這個房間打掃完,你們纔可以扔。

在這個場景中,一個人扔,一個人掃,就類似於Minor GC,這時候,並不會影響扔垃圾的人,然後一旦10個人同時仍,而且很快就沒地方仍了,這時候,就會觸發Full GC,然後JVM下令,你們暫時都別仍了,等我什麼時候回收完垃圾了,你們在仍,現在大家清楚了吧,所謂的10個人,就是類似我們成千上百的java類, 在不停的執行任務,所謂的清潔工,就是我們的GC機制,所以,大家在平時編碼的時候,一定注意儘量少造點垃圾對象,這樣觸發FULL GC的機率,纔會變小。

 

發佈了364 篇原創文章 · 獲贊 389 · 訪問量 141萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章