天天看點

JVM垃圾回收

JVM 垃圾回收

寫在前面

本節常見面試題

本文導火索

1 揭開 JVM 記憶體配置設定與回收的神秘面紗

1.1 對象優先在 eden 區配置設定

1.2 大對象直接進入老年代

1.3 長期存活的對象将進入老年代

1.4 動态對象年齡判定

1.5 主要進行 gc 的區域

2 對象已經死亡?

2.1 引用計數法

2.2 可達性分析算法

2.3 再談引用

2.4 不可達的對象并非“非死不可”

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

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

3 垃圾收集算法

3.1 标記-清除算法

3.2 标記-複制算法

3.3 标記-整理算法

3.4 分代收集算法

4 垃圾收集器

4.1 Serial 收集器

4.2 ParNew 收集器

4.3 Parallel Scavenge 收集器

4.4.Serial Old 收集器

4.5 Parallel Old 收集器

4.6 CMS 收集器

4.7 G1 收集器

4.8 ZGC 收集器

參考

問題答案在文中都有提到

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

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

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

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

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

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

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

介紹一下 CMS,G1 收集器。

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

JVM垃圾回收

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

Java 的自動記憶體管理主要是針對對象記憶體的回收和對象記憶體的配置設定。

同時,Java 自動記憶體管理最核心的功能是 堆 記憶體中對象的配置設定與回收。

Java 堆是垃圾收集器管理的主要區域,是以也被稱作GC 堆(Garbage Collected Heap).

從垃圾回收的角度,由于現在收集器基本都采用分代垃圾收集算法,是以 Java 堆還可以細分為:新生代和老年代:再細緻一點有:Eden 空間、From Survivor、To Survivor 空間等。

進一步劃分的目的是更好地回收記憶體,或者更快地配置設定記憶體。

堆空間的基本結構:

JVM垃圾回收

 上圖所示的 Eden 區、From Survivor0("From") 區、To Survivor1("To") 區都屬于新生代,Old Memory 區屬于老年代。

大部分情況,對象都會首先在 Eden 區域配置設定,在一次新生代垃圾回收後,如果對象還存活,則會進入 s0 或者 s1,并且對象的年齡還會加 1(Eden 區->Survivor 區後對象的初始年齡變為 1),

當它的年齡增加到一定程度(預設為大于 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡門檻值,可以通過參數 ​<code>​-XX:MaxTenuringThreshold​</code>​ 來設定預設值,

這個值會在虛拟機運作過程中進行調整,可以通過​<code>​-XX:+PrintTenuringDistribution​</code>​來列印出當次GC後的Threshold。

Hotspot 周遊所有對象時,按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了 survivor 區的一半時,取這個年齡和 MaxTenuringThreshold 中更小的一個值,作為新的晉升年齡門檻值”。 動态年齡計算的代碼如下

經過這次 GC 後,Eden 區和"From"區已經被清空。這個時候,"From"和"To"會交換他們的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To"。

不管怎樣,都會保證名為 To 的 Survivor 區域是空的。Minor GC 會一直重複這樣的過程,在這個過程中,有可能當次Minor GC後,Survivor 的"From"區域空間不夠用,有一些還達不到進入老年代條件的執行個體放不下,則放不下的部分會提前進入老年代。

接下來我們提供一個調試腳本來測試這個過程。

調試代碼參數如下

示例代碼如下:

注意:如下輸出結果中老年代的資訊為 ​<code>​concurrent mark-sweep generation​</code>​ 和以前版本略有不同。另外,還列出了某次GC後是否重新生成了threshold,以及各個年齡占用空間的大小。

JVM垃圾回收

目前主流的垃圾收集器都會采用分代回收算法,是以需要将堆記憶體分為新生代和老年代,這樣我們就可以根據各個年代的特點選擇合适的垃圾收集算法。

大多數情況下,對象在新生代中 eden 區配置設定。當 eden 區沒有足夠空間進行配置設定時,虛拟機将發起一次 Minor GC.下面我們來進行實際測試以下。

測試:

通過以下方式運作:

JVM垃圾回收

 添加的參數:​<code>​-XX:+PrintGCDetails​</code>​

JVM垃圾回收

 運作結果 (紅色字型描述有誤,應該是對應于 JDK1.7 的永久代):

JVM垃圾回收

 從上圖我們可以看出 eden 區記憶體幾乎已經被配置設定完全(即使程式什麼也不做,新生代也會使用 2000 多 k 記憶體)。假如我們再為 allocation2 配置設定記憶體會出現什麼情況呢?

JVM垃圾回收

 簡單解釋一下為什麼會出現這種情況: 因為給 allocation2 配置設定記憶體的時候 eden 區記憶體幾乎已經被配置設定完了,我們剛剛講了當 Eden 區沒有足夠空間進行配置設定時,虛拟機将發起一次 Minor GC.GC 期間虛拟機又發現 allocation1 無法存入 Survivor 空間,是以隻好通過 配置設定擔保機制 把新生代的對象提前轉移到老年代中去,老年代上的空間足夠存放 allocation1,是以不會出現 Full GC。執行 Minor GC 後,後面配置設定的對象如果能夠存在 eden 區的話,還是會在 eden 區配置設定記憶體。可以執行如下代碼驗證:

大對象就是需要大量連續記憶體空間的對象(比如:字元串、數組)。

為什麼要這樣呢?

為了避免為大對象配置設定記憶體時由于配置設定擔保機制帶來的複制而降低效率。

既然虛拟機采用了分代收集的思想來管理記憶體,那麼記憶體回收時就必須能識别哪些對象應放在新生代,哪些對象應放在老年代中。為了做到這一點,虛拟機給每個對象一個對象年齡(Age)計數器。

如果對象在 Eden 出生并經過第一次 Minor GC 後仍然能夠存活,并且能被 Survivor 容納的話,将被移動到 Survivor 空間中,并将對象年齡設為 1.對象在 Survivor 中每熬過一次 MinorGC,年齡就增加 1 歲,當它的年齡增加到一定程度(預設為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡門檻值,可以通過參數 ​<code>​-XX:MaxTenuringThreshold​</code>​ 來設定。

大部分情況,對象都會首先在 Eden 區域配置設定,在一次新生代垃圾回收後,如果對象還存活,則會進入 s0 或者 s1,并且對象的年齡還會加 1(Eden 區-&gt;Survivor 區後對象的初始年齡變為 1),當它的年齡增加到一定程度(預設為 15 歲),就會被晉升到老年代中。對象晉升到老年代的年齡門檻值,可以通過參數 ​<code>​-XX:MaxTenuringThreshold​</code>​ 來設定。

修正(issue552):“Hotspot 周遊所有對象時,按照年齡從小到大對其所占用的大小進行累積,當累積的某個年齡大小超過了 survivor 區的 50% 時(預設值是 50%,可以通過 ​<code>​-XX:TargetSurvivorRatio=percent​</code>​ 來設定,參見 issue1199 ),取這個年齡和 MaxTenuringThreshold 中更小的一個值,作為新的晉升年齡門檻值”。 動态年齡計算的代碼如下:

額外補充說明(issue672):關于預設的晉升年齡是 15,這個說法的來源大部分都是《深入了解 Java 虛拟機》這本書。 如果你去 Oracle 的官網閱讀相關的虛拟機參數,你會發現​<code>​-XX:MaxTenuringThreshold=threshold​</code>​這裡有個說明

Sets the maximum tenuring threshold for use in adaptive GC sizing. The largest value is 15. The default value is 15 for the parallel (throughput) collector, and 6 for the CMS collector.預設晉升年齡并不都是 15,這個是要區分垃圾收集器的,CMS 就是 6.

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

部分收集 (Partial GC):

新生代收集(Minor GC / Young GC):隻對新生代進行垃圾收集;

老年代收集(Major GC / Old GC):隻對老年代進行垃圾收集。需要注意的是 Major GC 在有的語境中也用于指代整堆收集;

混合收集(Mixed GC):對整個新生代和部分老年代進行垃圾收集。

整堆收集 (Full GC):收集整個 Java 堆和方法區。

空間配置設定擔保是為了確定在 Minor GC 之前老年代本身還有容納新生代所有對象的剩餘空間。

《深入了解Java虛拟機》第三章對于空間配置設定擔保的描述如下:

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

堆中幾乎放着所有的對象執行個體,對堆垃圾回收前的第一步就是要判斷哪些對象已經死亡(即不能再被任何途徑使用的對象)。

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

這個方法實作簡單,效率高,但是目前主流的虛拟機中并沒有選擇這個算法來管理記憶體,其最主要的原因是它很難解決對象之間互相循環引用的問題。 所謂對象之間的互相引用問題,如下面代碼所示:除了對象 objA 和 objB 互相引用着對方之外,這兩個對象之間再無任何引用。但是他們因為互相引用對方,導緻它們的引用計數器都不為 0,于是引用計數算法無法通知 GC 回收器回收他們。

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

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

虛拟機棧(棧幀中的本地變量表)中引用的對象

本地方法棧(Native 方法)中引用的對象

方法區中類靜态屬性引用的對象

方法區中常量引用的對象

所有被同步鎖持有的對象

無論是通過引用計數法判斷對象引用數量,還是通過可達性分析法判斷對象的引用鍊是否可達,判定對象的存活都與“引用”有關。

JDK1.2 之前,Java 中引用的定義很傳統:如果 reference 類型的資料存儲的數值代表的是另一塊記憶體的起始位址,就稱這塊記憶體代表一個引用。

JDK1.2 以後,Java 對引用的概念進行了擴充,将引用分為強引用、軟引用、弱引用、虛引用四種(引用強度逐漸減弱)

1.強引用(StrongReference)

以前我們使用的大部分引用實際上都是強引用,這是使用最普遍的引用。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當記憶體空間不足,Java 虛拟機甯願抛出 OutOfMemoryError 錯誤,使程式異常終止,也不會靠随意回收具有強引用的對象來解決記憶體不足問題。

2.軟引用(SoftReference)

如果一個對象隻具有軟引用,那就類似于可有可無的生活用品。如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些對象的記憶體。隻要垃圾回收器沒有回收它,該對象就可以被程式使用。軟引用可用來實作記憶體敏感的高速緩存。

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

3.弱引用(WeakReference)

如果一個對象隻具有弱引用,那就類似于可有可無的生活用品。弱引用與軟引用的差別在于:隻具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它所管轄的記憶體區域的過程中,一旦發現了隻具有弱引用的對象,不管目前記憶體空間足夠與否,都會回收它的記憶體。不過,由于垃圾回收器是一個優先級很低的線程, 是以不一定會很快發現那些隻具有弱引用的對象。

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

4.虛引用(PhantomReference)

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

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

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

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

即使在可達性分析法中不可達的對象,也并非是“非死不可”的,這時候它們暫時處于“緩刑階段”,要真正宣告一個對象死亡,至少要經曆兩次标記過程;

可達性分析法中不可達的對象被第一次标記并且進行一次篩選,篩選的條件是此對象是否有必要執行 finalize 方法。當對象沒有覆寫 finalize 方法,或 finalize 方法已經被虛拟機調用過時,虛拟機将這兩種情況視為沒有必要執行。

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

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

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

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

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

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

該類所有的執行個體都已經被回收,也就是 Java 堆中不存在該類的任何執行個體。

加載該類的 ​<code>​ClassLoader​</code>​ 已經被回收。

該類對應的 ​<code>​java.lang.Class​</code>​ 對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。

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