JVM常見面試題(四):垃圾回收

BJRA發表於2024-11-24

文章目錄

前言

  • 堆區域劃分
  • GC分類
  • 空間分配擔保
  • 檢視JDK使用的垃圾回收器
  • 常見面試題

一、物件什麼時候可以被垃圾器回收

  • 1.1 物件何時被垃圾器回收

  • 1.2 如何定位垃圾/判斷物件是否死亡

    • 1.2.1 引用計數法
    • 1.2.2 可達性分析演算法
  • 1.3 如何判斷一個常量是廢棄常量

  • 1.4 如何判斷一個類是無用的類

二、JVM 垃圾回收演算法有哪些

  • 2.1 標記清除演算法
  • 2.2 標記整理演算法
  • 2.3 複製演算法
  • 2.4 分代收集演算法

三、說一下JVM中的分代回收

  • 3.1 堆區域劃分
  • 3.2 分代收集演算法—工作機制/回收策略
  • 3.3 GC分類/MinorGC、Mixed GC、FullGC的區別是什麼

四、JVM有哪些垃圾回收器

  • 4.1 序列垃圾收集器

  • 4.2 並行垃圾收集器

    • Parallel New、Parallel Old
    • Parallel Scavenge收集器
  • 4.3 CMS(併發)垃圾收集器

  • 4.4 G1垃圾回收器

  • 4.5 ZGC垃圾回收器

五、詳細聊一下G1垃圾回收器

  • G1回收器詳解
  • Young Collection(年輕代垃圾回收)
  • Young Collection + Concurrent Mark (年輕代垃圾回收+併發標記)
  • Mixed Collection (混合垃圾回收)

六、強引用、軟引用、弱引用、虛引用

  • 6.1 強引用(StrongReference)
  • 6.2 軟引用(SoftReference)
  • 6.3 弱引用(WeakReference)
  • 6.4 虛引用(PhantomReference)

七、總結

前言

當需要排查各種記憶體溢位問題、當垃圾收整合為系統達到更高併發的瓶頸時,我們就需要對這些“自動化"的技術實施必要的監控和調節。

  • Java 的自動記憶體管理主要是針對物件記憶體的回收和物件記憶體的分配。同時,Java自動記憶體管理最核心的功能是記憶體中物件的分配與回收。
  • Java堆是垃圾收集器管理的主要區域,因此也被稱作GC堆(Garbage Collected Heap)
  • 從垃圾回收的角度來說,由於現在收集器基本都採用分代垃圾收集演算法,所以Java堆被劃分為了幾個不同的區域,這樣我們就可以根據各個區域的特點選擇合適的垃圾收集演算法。

堆區域劃分


GC分類

針對HotSpot VM的實現,它裡面的GC其實準確分類只有兩大種:

  • 部分收集(Partial GC):
    • 新生代收集(Minor GC/Young GC):只對新生代進行垃圾收集;
    • 老年代收集(Major GC/Old GC):只對老年代進行垃圾收集。需要注意的是Major GC在有的語境中也用於指代整堆收集;
    • 混合收集(Mixed GC):對整個新生代和部分老年代進行垃圾收集。
  • 整堆收集(Full GC):收集整個Java堆和方法區。新生代 + 老年代完整垃圾回收

空間分配擔保

空間分配擔保是為了確保在Minor GC之前老年代本身還有容納新生代所有物件的剩餘空間。

《深入理解Java虛擬機器》第三章對於空間分配擔保的描述如下:

  • JDK6 Update 24之前,在發生Minor GC之前,虛擬機器必須先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那這一次Minor GC可以確保是安全的。如果不成立,則虛擬機器會先檢視-XX:HandlePromotionFailure 引數的設定值是否允許擔保失敗(Handle
    Promotion Failure);如果允許,那會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者-XX:HandlePromotionFailure 設定不允許冒險,那這時就要改為進行一次Full GC。
  • JDK6 Update 24之後的規則變為只要老年代的連續空間大於新生代物件總大小或者歷次晉升的平均大小,就會進行Minor GC,否則將進行Full GC。

檢視JDK使用的垃圾回收器

JDK預設垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version命令檢視):

  • JDK 8:Parallel Scavenge (新生代)+ Parallel Old (老年代)
  • JDK 9 ~ JDK22:G1
#檢視被手動指定的引數。關注結果中的 UsexxxxGC 引數,如果沒有指定,則使用預設收集器
java -XX:+PrintCommandLineFlags -version  

#檢視所有引數
java -XX:+PrintFlagsFinal -version

#過濾檢視GC相關引數。 這將列出所有與GC相關的引數。如果所有相關引數都為 false,則使用預設收集器。
java -XX:+PrintFlagsFinal -version | grep .*Use.*GC.*      #Linux系統
java -XX:+PrintFlagsFinal -version | findstr .*Use.*GC.*   #Windows系統

上圖表面jdk1.8使用的是預設的ParallelGC組合,即新生代Parallel Scavenge和老年代Parallel Old。

常見面試題

  • 如何判斷物件是否死亡/如何定位垃圾(兩種方法)
  • 簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)
  • 如何判斷一個常量是廢棄常量
  • 如何判斷一個類是無用的類
  • 垃圾收集有哪些演算法,各自的特點?
  • HotSpot為什麼要分為新生代和老年代?
  • 常見的垃圾回收器有哪些?
  • 介紹一下CMS,G1收集器
  • Minor GC和Full GC有什麼不同呢?

一、物件什麼時候可以被垃圾器回收

1.1 物件何時被垃圾器回收

  • 如果一個或多個物件沒有任何的引用指向它了,那麼這個物件現在就是垃圾,如果定位了垃圾,則有可能會被垃圾回收器回收。
  • 如果要定位什麼是垃圾,有兩種方式來確定,第一個是引用計數法,第二個是可達性分析演算法

1.2 如何定位垃圾/判斷物件是否死亡

如果要定位什麼是垃圾,有兩種方式來確定,第一個是引用計數法,第二個是可達性分析演算法

1.2.1 引用計數法

一個物件被引用了一次,在當前的物件頭上遞增一次引用次數,如果這個物件的引用次數為0,代表這個物件可回收

這個方法實現簡單,效率高,但是目前主流的虛擬機器中並沒有選擇這個演算法來管理記憶體,其最主要的原因是它很難解決物件之間迴圈引用的問題。

當物件間出現了迴圈引用的話,則引用計數法就會失效。

1.2.2 可達性分析演算法

現在的虛擬機器採用的都是透過可達性分析演算法來確定哪些內容是垃圾。

這個演算法的基本思想就是透過一系列的稱為“GC Roots”的物件作為起點,從這些節點開始向下搜尋,節點所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連的話,則證明此物件是不可用的,需要被回收。

哪些物件可以作為 GC Root ?

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

重點看前三種

物件可以被回收,就代表一定會被回收嗎?

即使在可達性分析法中不可達的物件,也並非是“非死不可”的,這時候它們暫時處於“緩刑階段”,要真正宣告一個物件死亡,至少要經歷兩次標記過程;可達性分析法中不可達的物件被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行finalize方法。當物件沒有覆蓋finalize方法,或finalize方法已經被虛擬機器呼叫過時,虛擬機器將這兩種情況視為沒有必要執行。

被判定為需要執行的物件將會被放在一個佇列中進行第二次標記,除非這個物件與引用鏈上的任何一個物件建立關聯,否則就會被真的回收。

Object類中的finalize方法一直被認為是一個糟糕的設計,成為了Java語言的負擔,影響了Java語言的安全和GC的效能。JDK9版本及後續版本中各個類中的finalize方法會被逐漸棄用移除。忘掉它的存在吧!

無論是透過引用計數法判斷物件引用數量,還是透過可達性分析法判斷物件的引用鏈是否可達,判定物件的存活都與“引用”有關。
JDK1.2之前,Java中引用的定義很傳統:如果reference型別的資料儲存的數值代表的是另一塊記憶體的起始地址,就稱這塊記憶體代表一個引用。
JDK1.2以後,Java對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)

引用型別詳解可見本文第六節。

1.3 如何判斷一個常量是廢棄常量

執行時常量池主要回收的是廢棄的常量。那麼,我們如何判斷一個常量是廢棄常量呢?

  1. JDK1.7之前執行時常量池邏輯包含字串常量池存放在方法區,此時hotspot虛擬機器對方法區的實現為永久代
  2. JDK1.7字串常量池被從方法區拿到了堆中,這裡沒有提到執行時常量池,也就是說字串常量池被單獨拿到堆,執行時常量池剩下的東西還在方法區,也就是hotspot中的永久代,
  3. JDK1.8 hotspot移除了永久代用元空間(Metaspace)取而代之,這時候字符串常量池還在堆,執行時常量池還在方法區,只不過方法區的實現從永久代變成了元空間(IMetaspace)

假如在字串常量池中存在字串"abe",如果當前沒有任何String物件引用該字串常量的話,就說明常量"abc"就是廢棄常量,如果這時發生記憶體回收的話而且有必要的話,"abc"就會被系統清理出常量池了。

1.4 如何判斷一個類是無用的類

方法區主要回收的是無用的類,那麼如何判斷一個類是無用的類的呢?

判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:

  • 該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項。
  • 載入該類的ClassLoader 已經被回收。
  • 該類對應的java.lang.Class 物件沒有在任何地方被引用,無法在任何地方透過反射訪問該類的方法。

虛擬機器可以對滿足上述3個條件的無用類進行回收,這裡說的僅僅是“可以”,而並不是和物件一樣不使用了就會必然被回收。

二、JVM 垃圾回收演算法有哪些

  • 標記清除演算法
  • 複製演算法
  • 標記整理演算法

2.1 標記清除演算法

標記清除演算法(Mark-and-Sweep),是將垃圾回收分為2個階段,分別是標記(Mark)和清除(Sweep)

  1. 根據可達性分析演算法得出的垃圾進行標記
  2. 對這些標記為可回收的內容進行垃圾回收

它是最基礎的收集演算法,後續的演算法都是對其不足進行改進得到。這種垃圾收集演算法會帶來兩個明顯的問題:

  • 效率問題:標記和清除兩個過程效率都不高
  • 空間問題:標記清除後會產生大量不連續的記憶體碎片,

關於具體是標記可回收物件(垃圾)還是不可回收物件,眾說紛運,兩種說法其實都沒問題,我個人更傾向於是前者。

如果按照前者的理解,整個標記-清除過程大致是這樣的:

  1. 當一個物件被建立時,給一個標記位,假設為o(false);
  2. 在標記階段,我們將所有可達物件(或使用者可以引用的物件)的標記位設定為1(true);
  3. 掃描階段清除的就是標記位為o(false)的物件。

2.2 標記整理演算法

標記-整理(Mark-and-Compact)演算法是根據老年代的特點提出的一種標記演算法,標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件回收,而是讓所有存活的物件向一端移動,然後直接清理掉端邊界以外的記憶體。

優缺點同標記清除演算法,解決了標記清除演算法的碎片化的問題,同時標記壓縮演算法多了一步,物件移動記憶體位置的步驟,其效率也有有一定的影響。

由於多了整理這一步,因此效率也不高,適合老年代這種垃圾回收頻率不是很高的場景。

2.3 複製演算法

複製演算法(Copying)將記憶體分為大小相同的兩塊,每次使用其中的一塊。當這一塊的記憶體使用完後,就將還存活的物件複製到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的記憶體回收都是對記憶體區間的一半進行回收。

優點:

  • 在垃圾物件多的情況下,效率較高
  • 清理後,記憶體無碎片

雖然改進了標記-清除演算法,但依然存在下面這些問題:

  • 可用記憶體變小:可用記憶體縮小為原來的一半,記憶體使用率較低。
  • 不適合老年代:如果存活物件數量比較大,複製效能會變得很差。

2.4 分代收集演算法

當前虛擬機器的垃圾收集都採用分代收集演算法,這種演算法沒有什麼新的思想,只是根據物件存活週期的不同將記憶體分為幾塊。一般將Java堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集演算法。

比如在新生代中,每次收集都會有大量物件死去,所以可以選擇“複製”演算法,只需要付出少量物件的複製成本就可以完成每次垃圾收集。而老年代的物件存活機率是比較高的,而且沒有額外的空間對它進行分配擔保,所以我們必須選擇“標記-清除”或“標記-整理”演算法進行垃圾收集。

具體細節可查閱本文第三節。

延伸面試問題:HotSpot為什麼要分為新生代和老年代?

根據上面的對分代收集演算法的介紹回答。(可以根據各個年代的特點選擇合適的垃圾收集演算法)

三、說一下JVM中的分代回收

3.1 堆區域劃分

Java8-JVM記憶體結構:

在java8時,堆被分為了兩份:新生代和老年代【1:2】

對於新生代,內部又被分為了三個區域:Eden、from、to

  • 伊甸園區Eden,新生的物件都分配到這裡
  • 倖存者區survivor(分成from和to)
  • Eden區,from區,to區【8:1:1】

3.2 分代收集演算法—工作機制/回收策略

  • 新建立的物件,都會先分配到eden區
  • 當伊甸園記憶體不足,標記伊甸園與 from(現階段沒有)的存活物件
  • 將存活物件採用複製演算法複製到 to 中,複製完畢後,伊甸園和 from 記憶體都得到釋放
  • 經過一段時間後伊甸園的記憶體又出現不足,標記eden區域、to區存活的物件,將存活的物件複製到from區
  • 當倖存區物件熬過幾次回收(最多15次),晉升到老年代(倖存區記憶體不足或大物件會導致提前晉升)

3.3 GC分類/MinorGC、Mixed GC、FullGC的區別是什麼

針對HotSpot VM的實現,它裡面的GC其實準確分類只有兩大種:

  • 部分收集(Partial GC):
    • 新生代收集(Minor GC/Young GC):只對新生代進行垃圾收集。發生在新生代的垃圾回收,暫停時間短(STW)
    • 老年代收集(Major GC/Old GC):只對老年代進行垃圾收集。需要注意的是Major GC在有的語境中也用於指代整堆收集;
    • 混合收集(Mixed GC):對整個新生代和部分老年代進行垃圾收集。新生代 + 老年代部分割槽域的垃圾回收,G1 收集器特有(只有G1有這個模式)
  • 整堆收集(Full GC):收集整個Java堆和方法區。新生代 + 老年代完整垃圾回收,暫停時間長(STW),應盡力避免

STW(Stop-The-World):暫停所有應用程式執行緒,等待垃圾回收的完成

四、JVM有哪些垃圾回收器

如果說收集演算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實現。

雖然我們對各個收集器進行比較,但並非要挑選出一個最好的收集器。因為直到現在為止還沒有最好的垃圾收集器出現,更加沒有萬能的垃圾收集器,我們能做的就是根據具體應用場景選擇適合自己的垃圾收集器。試想一下:如果有一種四海之內、任何場景下都適用的完美收集器存在,那麼我們的HotSpot虛擬機器就不會實現那麼多不同的垃圾收集器了。

JDK預設垃圾收集器(使用 java -XX:+PrintCommandLineFlags -version命令檢視):

  • JDK 8:Parallel Scavenge (新生代)+ Parallel Old (老年代)
  • JDK 9 ~ JDK22:G1

在jvm中,實現了多種垃圾收集器,包括:

  • 序列垃圾收集器:Serial GC、Serial Old GC
  • 並行垃圾收集器:Parallel Old GC、ParNew GC
  • CMS(併發)垃圾收集器:CMS GC,作用在老年代
  • G1垃圾收集器,作用在新生代和老年代

4.1 序列垃圾收集器

Serial(序列)收集器是最基本、歷史最悠久的垃圾收集器了。大家看名字就知道這個收集器是一個單執行緒收集器了。它的“單執行緒”的意義不僅僅意味著它只會使用一條垃圾收集執行緒去完成垃圾收集工作,更重要的是它在進行垃圾收集工作的時候必須暫停其他所有的工作執行緒("StopTheWorld"),直到它收集結束。

新生代採用複製演算法,老年代採用標記-整理演算法。

SerialSerial Old序列垃圾收集器,是指使用單執行緒進行垃圾回收,堆記憶體較小,適合個人電腦

  • Serial 作用於新生代,採用複製演算法
  • Serial Old 作用於老年代,採用標記-整理演算法

垃圾回收時,只有一個執行緒在工作,並且java應用中的所有執行緒都要暫停(STW),等待垃圾回收的完成。

虛擬機器的設計者們當然知道StopTheWorld帶來的不良使用者體驗,所以在後續的垃圾收集器設計中停頓時間在不斷縮短(仍然還有停頓,尋找最優秀的垃圾收集器的過程仍然在繼續)。

但是Serial收集器有沒有優於其他垃圾收集器的地方呢?當然有,它簡單而高效(與其他收集器的單執行緒相比)。Serial收集器由於沒有執行緒互動的開銷,自然可以獲得很高的單執行緒收集效率。Serial收集器對於執行在Client模式下的虛擬機器來說是個不錯的選擇。

4.2 並行垃圾收集器

Parallel New、Parallel Old

Parallel New和Parallel Old是一個並行垃圾回收器,JDK8預設使用此垃圾回收器

  • Parallel New作用於新生代,採用複製演算法
  • Parallel Old作用於老年代,採用標記-整理演算法。Parallel Scavenge收集器的老年代版本

垃圾回收時,多個執行緒在工作,並且java應用中的所有執行緒都要暫停(STW),等待垃圾回收的完成。

Parallel Scavenge收集器

Parallel Scavenge收集器也是使用標記-複製演算法的多執行緒收集器,它看上去幾乎和ParNew都一樣。那麼它有什麼特別之處呢?

-XX:+UseParallelGC        使用 Parallel收集器+老年代序列
-XX:+UseParallelOldGC     使用 Parallel收集器+老年代並行

Parallel Scavenge收集器關注點是吞吐量(高效率的利用CPU)。CMS等垃圾收集器的關注點更多的是使用者執行緒的停頓時間(提高使用者體驗)。所謂吞吐量就是CPU中用於執行使用者程式碼的時間與CPU總消耗時間的比值。Parallel Scavenge收集器提供了很多引數供使用者找到最合適的停頓時間或最大吞吐量,如果對於收集器運作不太瞭解,手工最佳化存在困難的時候,使用Parallel Scavenge收集器配合自適應調節策略,把記憶體管理最佳化交給虛擬機器去完成也是一個不錯的選擇。

新生代採用標記-複製演算法,老年代採用標記-整理演算法。

這是JDK1.8預設垃圾收集器,即Parallel Scavenge (新生代)+ Parallel Old (老年代)。使用 java -XX:+PrintCommandLineFlags -version命令直接在命令列檢視

#檢視被手動指定的引數。關注結果中的 UsexxxxGC 引數,如果沒有指定,則使用預設收集器
java -XX:+PrintCommandLineFlags -version  

#檢視所有引數
java -XX:+PrintFlagsFinal -version

#過濾檢視GC相關引數。 這將列出所有與GC相關的引數。如果所有相關引數都為 false,則使用預設收集器。
java -XX:+PrintFlagsFinal -version | grep .*Use.*GC.*      #Linux系統
java -XX:+PrintFlagsFinal -version | findstr .*Use.*GC.*   #Windows系統

上圖表面jdk1.8使用的是預設的ParallelGC組合,即新生代Parallel Scavenge和老年代Parallel Old。

JDKi.8預設使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC 引數,則預設指定了 -XX:+UseParallelOldGC,可以使用-XX:-UseParalleloldGC來禁用該功能。

4.3 CMS(併發)垃圾收集器

CMS全稱 Concurrent Mark Sweep,是一款併發的、使用標記-清除演算法的垃圾回收器,該回收器是針對老年代垃圾回收的,是一款以獲取最短回收停頓時間為目標的收集器,停頓時間短,使用者體驗就好。其最大特點是在進行垃圾回收時,應用仍然能正常執行。

CMS收集器是HotSpot虛擬機器第一款真正意義上的併發收集器,它第一次實現了讓垃圾收集執行緒與使用者執行緒(基本上)同時工作。從名字中的Mark Sweep這兩個詞可以看出,CMS收集器是一種“標記-清除"演算法實現的,它的運作過程相比於前面幾種垃圾收集器來說更加複雜一些。整個過程分為四個步驟:

  • 初始標記:暫停所有的其他執行緒,並記錄下直接與root相連的物件,速度很快;
  • 併發標記:同時開啟GC和使用者執行緒,用一個閉包結構去記錄可達物件。但在這個階段結束,這個閉包結構並不能保證包含當前所有的可達物件。因為使用者執行緒可能會不斷的更新引用域,所以GC執行緒無法保證可達性分析的實時性。所以這個演算法裡會跟蹤記錄這些發生引用更新的地方。
  • 重新標記:重新標記階段就是為了修正併發標記期間因為使用者程式繼續執行而導致標記產生變動的那一部分物件的標記記錄,這個階段的停頓時間一般會比初始標記階段的時間稍長,遠遠比並發標記階段時間短
  • 併發清除:開啟使用者執行緒,同時GC執行緒開始對未標記的區域做清掃。

(第一步是找出有哪些 gc root,第二步是順著 gc root,第三步查詢遺漏的物件。)

優缺點

  • 優點:併發收集,低停頓

  • 缺點

    • 對CPU資源敏感;
    • 無法處理浮動垃圾;
    • 它使用的回收演算法-“標記-清除”演算法會導致收集結束時會有大量空間碎片產生。

為什麼有重新標記?

因為併發標記過程中其他執行緒是執行的,可能有新的物件可以被回收、或者引用發生改變,所以需要重新標記。

以下圖為例,本來X為垃圾,但在併發標記階段 A物件引用X物件,此時X不是垃圾、不能被回收,故需要重新標記

CMS垃圾回收器在Java9中已經被標記為過時(deprecated),並在Java14中被移除。

4.4 G1垃圾回收器

G1(Garbage-First)是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器。以極高機率滿足GC停頓時間要求的同時,還具備高吞吐量效能特徵。

具體細節可查閱本文第五節

4.5 ZGC垃圾回收器

與 CMS 中的 ParNew和 G1 類似,ZGC 也採用標記-複製演算法,不過 ZGC 對該演算法做了重大改進。

ZGC可以將暫停時間控制在幾毫秒以內,且暫停時間不受堆記憶體大小的影響,出現Stop The World的情況會更少,但代價是犧牲了一些吞吐量。ZGC最大支援16TB的堆記憶體。

ZGC在Java11中引入,處於試驗階段。經過多個版本的選代,不斷的完善和修復問題,ZGC在Java15已經可以正式使用了。不過,預設的垃圾回收器依然是G1。你可以透過下面的引數啟用ZGC:

java -XX:+UseZGC className

在Java21中,引入了分代ZGC,暫停時間可以縮短到1毫秒以內。你可以透過下面的引數啟用分代ZGC:

java -XX:+UseZGC -XX:+ZGenerational className

關於 ZGC 收集器的詳細介紹推薦閱讀美團技術團隊的 新一代垃圾回收器ZGC的探索與實踐 這篇文章。

五、詳細聊一下G1垃圾回收器

G1回收器詳解

G1(Garbage-First)是一款面向伺服器的垃圾收集器,主要針對配備多顆處理器及大容量記憶體的機器。以極高機率滿足GC停頓時間要求的同時,還具備高吞吐量效能特徵。

  • 應用於新生代和老年代,在JDK9及之後預設使用G1
  • 劃分成多個區域,每個區域都可以充當 eden,survivor,old, humongous,其中 humongous 專為大物件準備
  • 採用複製演算法
  • 響應時間與吞吐量兼顧
  • 分成三個階段:新生代回收、併發標記、混合收集
  • 如果併發失敗(即回收速度趕不上建立新物件速度),會觸發 Full GC

被視為JDK1.7中HotSpot虛擬機器的一個重要進化特徵。它具備以下特點:

  • 並行與併發:G1能充分利用CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短Stop-The-World停頓時間。部分其他收集器原本需要停頓Java執行緒執行的GC動作,Gi收集器仍然可以透過併發的方式讓java程式繼續執行。
  • 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。
  • 空間整合:與CMS的“標記-清除"演算法不同,G1從整體來看是基於“標記-整理”演算法實現的收集器;從區域性上來看是基於“標記-複製”演算法實現的。
  • 可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M室秒的時間片段內,消耗在垃圾收集上的時間不得超過N掌秒。

G1收集器的運作大致分為以下幾個步驟:

  • 初始標記
  • 併發標記
  • 最終標記
  • 篩選回收

G1收集器在後臺維護了一個優先列表,每次根據允許的收集時間,優先選擇回收價值最大的Region(這也就是它的名字Garbage-First的由來)。這種使用Region劃分記憶體空間以及有優先順序的區域回收方式,保證了G1收集器在有限時間內可以儘可能高的收集效率(把記憶體化整為零)。

從JDK9開始,G1垃圾收集器成為了預設的垃圾收集器。

Young Collection(年輕代垃圾回收)

  • 初始時,所有區域都處於空閒狀態
  • 建立了一些物件,挑出一些空閒區域作為伊甸園區儲存這些物件
  • 當伊甸園需要垃圾回收時,挑出一個空閒區域作為倖存區,用複製演算法複製存活物件,需要暫停使用者執行緒
  • 隨著時間流逝,伊甸園的記憶體又有不足
  • 將伊甸園以及之前倖存區中的存活物件,採用複製演算法,複製到新的倖存區,其中較老物件晉升至老年代

Young Collection + Concurrent Mark (年輕代垃圾回收+併發標記)

當老年代佔用記憶體超過閾值(預設是45%)後,觸發併發標記,這時無需暫停使用者執行緒

  • 併發標記之後,會有重新標記階段解決漏標問題,此時需要暫停使用者執行緒。
  • 這些都完成後就知道了老年代有哪些存活物件,隨後進入混合收集階段。此時不會對所有老年代區域進行回收,而是根據暫停時間目標優先回收價值高(存活物件少)的區域(這也是 Gabage First 名稱的由來)。

Mixed Collection (混合垃圾回收)

混合收集階段中,參與複製的有 eden、survivor、old

複製完成,記憶體得到釋放。進入下一輪的新生代回收、併發標記、混合收集

六、強引用、軟引用、弱引用、虛引用

  • 無論是透過引用計數法判斷物件引用數量,還是透過可達性分析法判斷物件的引用鏈是否可達,判定物件的存活都與“引用”有關。
  • JDK1.2之前,Java中引用的定義很傳統:如果reference型別的資料儲存的數值代表的是另一塊記憶體的起始地址,就稱這塊記憶體代表一個引用。
  • JDK1.2以後,Java對引用的概念進行了擴充,將引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)

強引用、軟引用、弱引用、虛引用的區別

6.1 強引用(StrongReference)

一般把一個物件賦給一個引用變數,這個引用變數就是強引用。以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用,只要還有強引用指向一個物件,就能表明物件還“活著”、處於可達狀態,垃圾收集器不會碰這種物件

如果一個物件具有強引用,那就類似於必不可少的生活用品垃圾回收器絕不會回收它。當記憶體空間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足問題。

6.2 軟引用(SoftReference)

軟引用是用來描述一些還有用但並非必需的物件,需要用java.lang.ref.SoftReference類來實現。如果一個物件只具有軟引用,那就類似於可有可無的生活用品

在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收(在垃圾回收後,記憶體仍不足時會再次發出垃圾回收),如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。即如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體。只要垃圾回收器沒有回收它,該物件就可以被程式使用。軟引用可用來實現記憶體敏感的快取記憶體。

軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收,JAVA虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。

6.3 弱引用(WeakReference)

如果一個物件只具有弱引用,那就類似於可有可無的生活用品,和軟引用類似。弱引用與軟引用的區別在於:只具有弱引用的物件擁有更短暫的生命週期。在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間足夠與否,都會回收它的記憶體。不過,由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。

弱引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。

6.4 虛引用(PhantomReference)

"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。

虛引用主要用來跟蹤物件被垃圾回收的活動。

虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以透過判斷引用佇列中是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。程式如果發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。

特別注意,在程式設計中一般很少使用弱引用與虛引用,使用軟引用的情況較多,這是因為軟引用可以加速JVIM對垃圾記憶體的回收速度,可以維護系統的執行安全,防止記憶體溢位(OutOfMemory)等問題的產生

七、總結

1)物件什麼時候可以被垃圾器回收

如果一個或多個物件沒有任何的引用指向它了,那麼這個物件現在就是垃圾,如果定位了垃圾,則有可能會被垃圾回收器回收。

2)定位垃圾的方式有兩種

  • 引用計數法
  • 可達性分析演算法

3)JVM垃圾回收演算法有哪些

  • 標記清除演算法:垃圾回收分為2個階段,分別是標記和清除,效率高,有磁碟碎片,記憶體不連續
  • 標記整理演算法:標記清除演算法一樣,將存活物件都向記憶體另一端移動,然後清理邊界以外的垃圾,無碎片,物件需要移動,效率低
  • 複製演算法:將原有的記憶體空間一分為二,每次只用其中的一塊,正在使用的物件複製到另一個記憶體空間中,然後將該記憶體空間清空,交換兩個記憶體的角色,完成垃圾的回收;無碎片,記憶體使用率低

4)說一下JVM中的分代回收

  • 堆的區域劃分
    • 在java8時,堆被分為了兩份:新生代和老年代【1:2】
    • 對於新生代,內部又被分為了三個區域:Eden區,倖存者區survivor(分成from和to)【8:1:1】
  • 物件回收分代回收策略
    • 新建立的物件,都會先分配到eden區
    • 當伊甸園記憶體不足,標記伊甸園與 from(現階段沒有)的存活物件
    • 將存活物件採用複製演算法複製到 to 中,複製完畢後,伊甸園和 from 記憶體都得到釋放
    • 經過一段時間後伊甸園的記憶體又出現不足,標記eden區域、to區存活的物件,將存活的物件複製到from區
    • 當倖存區物件熬過幾次回收(最多15次),晉升到老年代(倖存區記憶體不足或大物件會導致提前晉升)

5)MinorGC、Mixed GC、FullGC的區別是什麼

  • MinorGC【young GC】:發生在新生代的垃圾回收,暫停時間短(STW)
  • Mixed GC:新生代 + 老年代部分割槽域的垃圾回收,G1 收集器特有(只有G1有這個模式)
  • FullGC: 整堆收集,新生代 + 老年代完整垃圾回收,暫停時間長(STW),應盡力避免

6)JVM有哪些垃圾回收器

在jvm中,實現了多種垃圾收集器,包括:

  • 序列垃圾收集器:Serial GC、Serial Old GC
  • 並行垃圾收集器:Parallel Old GC、ParNew GC
  • CMS(併發)垃圾收集器:CMS GC,作用在老年代
  • G1垃圾收集器,作用在新生代和老年代

7)詳細聊一下G1垃圾回收器

  • 應用於新生代和老年代,在JDK9及之後預設使用G1
  • 劃分成多個區域,每個區域都可以充當 eden,survivor,old, humongous,其中 humongous 專為大物件準備
  • 採用複製演算法
  • 響應時間與吞吐量兼顧
  • 分成三個階段:新生代回收(stw)、併發標記(重新標記stw)、混合收集
  • 如果併發失敗(即回收速度趕不上建立新物件速度),會觸發 Full GC

8)強引用、軟引用、弱引用、虛引用的區別

  • 強引用:必不可少的生活用品垃圾回收器絕不會回收它。只要所有 GC Roots 能找到,就不會被回收
  • 軟引用:可有可無的生活用品,如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體。需要配合SoftReference使用,當垃圾多次回收,記憶體依然不夠的時候會回收軟引用物件
  • 弱引用:需要配合WeakReference使用,僅有弱引用引用該物件時,在垃圾回收時,無論記憶體是否充足,都會回收弱引用物件
  • 虛引用:必須配合引用佇列使用,被引用物件回收時,會將虛引用入隊,由 Reference Handler 執行緒呼叫虛引用相關方法釋放直接記憶體

參考 黑馬程式設計師相關影片與文件、JavaGuide-JVM垃圾回收詳解

相關文章