一篇文章帶你瞭解JVM垃圾回收

如何判斷對象是否死亡(兩種方法)。

簡單的介紹一下強引用、軟引用、弱引用、虛引用(虛引用與軟引用和弱引用的區別、使用軟引用能帶來的好處)。

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

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

垃圾收集有哪些算法,各自的特點?

HotSpot 為什麼要分為新生代和老年代?

常見的垃圾回收器有哪些?

介紹一下 CMS,G1 收集器。

Minor Gc 和 Full GC 有什麼不同呢?

1.堆空間的基本結構:

現在的垃圾回收器基本上都采用分代垃圾回收算法,可分為新生代和老年代,新生代又可分為Eden區、From Survivor0、To Survivor區。

對象首先在eden區域分配,再經歷一次垃圾回收後,如果對象還存會,就年齡加一,並進入From Survive區,當年齡增加到一定程度(默認15歲)時,會進入老年代。對象進入老年代的年齡閾值可通過-Xx:MaxTenuringThreshold設置,這個值會在虛擬機運行過程中進行調整,HotSpot遍歷所有對象,按照年齡從小到大累計占用的區域,當累計區域超過一半時,取此時的年齡和設置的-Xx:MaxTenuringThreshold中較小值作為晉升老年代的年齡閾值。

在這裡插入圖片描述

在這裡插入圖片描述

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

部分收集 (Partial GC):

  • 新生代收集(Minor GC / Young GC):隻對新生代進行垃圾收集;
  • 老年代收集(Major GC / Old GC):隻對老年代進行垃圾收集。需要註意的是 Major GC 在有的語境中也用於指代整堆收集;
  • 混合收集(MixedGC):對整個新生代和部分老年代進行垃圾收集。

整堆收集 (Full GC):

  • 收集整個 Java 堆和方法區。

2.空間分配擔保機制

為瞭確保Major GC時,老年代有足夠的空間容納新生代的所有對象。

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

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

3.如何判斷一個對象已經無效

引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;任何時候計數器為 0 的對象就是不可能再被使用的。

這個方法實現簡單,效率高,但是目前主流的虛擬機中並沒有選擇這個算法來管理內存,其最主要的原因是它很難解決對象之間相互循環引用的問題。

2.2 可達性分析算法

這個算法的基本思想就是通過一系列的稱為 “GC Roots” 的對象作為起點,從這些節點開始向下搜索,節點所走過的路徑稱為

引用鏈,當一個對象到 GC Roots 沒有任何引用鏈相連的話,則證明此對象是不可用的。

可作為 GC Roots 的對象包括下面幾種:

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象
  • 本地方法棧(Native 方法)中引用的對象
  • 方法區中類靜態屬性引用的對象
  • 方法區中常量引用的對象
  • 所有被同步鎖持有的對象

4 不可達的對象並非“非死不可”

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

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

5 如何判斷一個常量是廢棄常量?

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

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

假如在字符串常量池中存在字符串 “abc”,如果當前沒有任何 String 對象引用該字符串常量的話,就說明常量 “abc” 就是廢棄常量,如果這時發生內存回收的話而且有必要的話,“abc” 就會被系統清理出常量池瞭。

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

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

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

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

7.垃圾回收算法

在這裡插入圖片描述

7.1 標記-清除算法

該算法分為“標記”和“清除”階段:首先標記出所有不需要回收的對象,在標記完成後統一回收掉所有沒有被標記的對象。

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

效率問題

空間問題(標記清除後會產生大量不連續的碎片)

7.2 標記-復制算法

為瞭解決效率問題,“標記-復制”收集算法出現瞭。它可以將內存分為大小相同的兩塊,每次使用其中的一塊。當這一塊的內存使用完後,就將還存活的對象復制到另一塊去,然後再把使用的空間一次清理掉。這樣就使每次的內存回收都是對內存區間的一半進行回收。

7.3 標記-整理算法

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

7.4 分代收集算法

當前虛擬機的垃圾收集都采用分代收集算法,這種算法沒有什麼新的思想,隻是根據對象存活周期的不同將內存分為幾塊。一般將 java 堆分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合適的垃圾收集算法。

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

總結

這篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: