深入理解 JVM 垃圾回收機制及其實現原理

文章目錄

前言

垃圾

什麼是垃圾?

垃圾判斷算法

引用計數法

可達性分析法

垃圾回收

垃圾回收算法

標記-清除算法

標記-整理算法

複製算法

分代收集算法

垃圾回收器

Serial 收集器

ParNew 收集器

Parallel Scavenge 收集器

Serial Old 收集器

Parallel Old 收集器

CMS收集器

G1 收集器

查看 JVM 使用的默認垃圾收集器

前言

對於 JVM 來說,我們都不陌生,其實 Java Virtual Machine(Java 虛擬機)的縮寫,它也是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM 有自己完善的硬件架構,如處理器、堆棧等,還具有相應的指令系統,其本質上就是一個程序,當它在命令行上啓動的時候,就開始執行保存在某字節碼文件中的指令。

Java 語言的可移植性就是建立在 JVM 的基礎之上的,任何平臺只要裝有針對於該平臺的 Java 虛擬機,字節碼文件(.class)就可以在該平臺上運行,這就是“一次編譯,多次運行”。除此之外,作爲 Java 語言最重要的特性之一的自動垃圾回收機制,也是基於 JVM 實現的。那麼,自動垃圾回收機制到底是如何實現的呢?在本文中,就讓我們一探究竟。

垃圾

什麼是垃圾?

在 JVM 進行垃圾回收之前,首先就是判斷哪些對象是垃圾,也就是說,要判斷哪些對象是可以被銷燬的,其佔有的空間是可以被回收的。根據 JVM 的架構劃分,我們知道, 在 Java 世界中,幾乎所有的對象實例都在堆中存放,所以垃圾回收也主要是針對堆來進行的。

在 JVM 的眼中,垃圾就是指那些在堆中存在的,已經“死亡”的對象。而對於“死亡”的定義,我們可以簡單的將其理解爲“不可能再被任何途徑使用的對象”。那怎樣才能確定一個對象是存活還是死亡呢?這就涉及到了垃圾判斷算法,其主要包括引用計數法和可達性分析法。

垃圾判斷算法

引用計數法

在這種算法中,假設堆中每個對象(不是引用)都有一個引用計數器。當一個對象被創建並且初始化賦值後,該對象的計數器的值就設置爲 1,每當有一個地方引用它時,計數器的值就加 1,例如將對象 b 賦值給對象 a,那麼 b 被引用,則將 b 引用對象的計數器累加 1。

反之,當引用失效時,例如一個對象的某個引用超過了生命週期(出作用域後)或者被設置爲一個新值時,則之前被引用的對象的計數器的值就減 1。而那些引用計數爲 0 的對象,就可以稱之爲垃圾,可以被收集。

特別地,當一個對象被當做垃圾收集時,它引用的任何對象的計數器的值都減 1。

優點:引用計數法實現起來比較簡單,對程序不被長時間打斷的實時環境比較有利。

缺點:需要額外的空間來存儲計數器,難以檢測出對象之間的循環引用。

可達性分析法

可達性分析法也被稱之爲根搜索法,可達性是指,如果一個對象會被至少一個在程序中的變量通過直接或間接的方式被其他可達的對象引用,則稱該對象就是可達的。更準確的說,一個對象只有滿足下述兩個條件之一,就會被判斷爲可達的:

  1. 對象是屬於更集中的對象
  2. 對象被一個可達的對象引用

在這裏,我們引出了一個專有名詞,即根集,其是指正在執行的 Java 程序可以訪問的引用變量(注意,不是對象)的集合,程序可以使用引用變量訪問對象的屬性和調用對象的方法。在 JVM 中,會將以下對象標記爲更集中的對象,具體包括:

  1. 虛擬機棧(棧幀中的本地變量表)中引用的對象
  2. 方法區中的常量引用的對象
  3. 方法區中的類靜態屬性引用的對象
  4. 本地方法棧中 JNI(Native 方法)的引用對象
  5. 活躍線程(已啓動且未停止的 Java 線程)

根集中的對象稱之爲GC Roots,也就是根對象。可達性分析法的基本思路是:將一系列的根對象作爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈,如果一個對象到根對象沒有任何引用鏈相連,那麼這個對象就不是可達的,也稱之爲不可達對象。

深入理解 JVM 垃圾回收機制及其實現原理

 

如上圖所示,形象的展示了可達對象與不可達對象的示例,其中灰色的對象都是不可達對象,表示可以被垃圾收集的對象。在可達性分析法中,對象有兩種狀態,那麼是可達的、要麼是不可達的,在判斷一個對象的可達性的時候,就需要對對象進行標記。關於標記階段,有幾個關鍵點是值得我們注意的,分別是:

開始進行標記前,需要先暫停應用線程,否則如果對象圖一直在變化的話是無法真正去遍歷它的。暫停應用線程以便 JVM 可以盡情地收拾家務的這種情況又被稱之爲安全點(Safe Point),這會觸發一次 Stop The World(STW)暫停。觸發安全點的原因有許多,但最常見的應該就是垃圾回收了。

安全點的選定基本上是以程序“是否具有讓程序長時間執行的特徵”爲標準進行選定的。“長時間執行”的最明顯特徵就是指令序列複用,例如方法調用、循環跳轉、異常跳轉等,所以具有這些功能的指令纔會產生安全點。對於安全點,另一個需要考慮的問題就是如何在 GC 發生時讓所有線程(這裏不包括執行 JNI 調用的線程)都“跑”到最近的安全點上再停頓下來。兩種解決方案:

搶先式中斷(Preemptive Suspension):搶先式中斷不需要線程的執行代碼主動去配合,在 GC 發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。現在幾乎沒有虛擬機採用這種方式來暫停線程從而響應 GC 事件。

主動式中斷(Voluntary Suspension):主動式中斷的思想是當 GC 需要中斷線程的時候,不直接對線程操作,僅僅簡單地設置一個標誌,各個線程執行時主動去輪詢這個標誌,發現中斷標誌爲真時就自己中斷掛起。輪詢標誌地地方和安全點是重合的,另外再加上創建對象需要分配內存的地方。

暫停時間的長短並不取決於堆內對象的多少也不是堆的大小,而是存活對象的多少。因此,調高堆的大小並不會影響到標記階段的時間長短。

在根搜索算法中,要真正宣告一個對象死亡,至少要經歷兩次標記過程:

如果對象在進行根搜索後發現沒有與根對象相連接的引用鏈,那它會被第一次標記並且進行一次篩選。篩選的條件是此對象是否有必要執行 finalize()方法(可看作析構函數,類似於 OC 中的dealloc,Swift 中的deinit)。當對象沒有覆蓋finalize()方法,或finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視爲沒有必要執行。

如果該對象被判定爲有必要執行finalize()方法,那麼這個對象將會被放置在一個名爲F-Queue的隊列中,並在稍後由一條由虛擬機自動建立的、低優先級的Finalizer線程去執行finalize()方法。finalize()方法是對象逃脫死亡命運的最後一次機會(因爲一個對象的finalize()方法最多隻會被系統自動調用一次),稍後 GC 將對F-Queue中的對象進行第二次小規模的標記,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中讓該對象重新引用鏈上的任何一個對象建立關聯即可。而如果對象這時還沒有關聯到任何鏈上的引用,那它就會被回收掉。

GC 判斷對象是否可達看的是強引用。

當標記階段完成後,GC 開始進入下一階段,刪除不可達對象。當然,可達性分析法有優點也有缺點,

優點:可以解決循環引用的問題,不需要佔用額外的空間

缺點:多線程場景下,其他線程可能會更新已經訪問過的對象的引用

在上面的介紹中,我們多次提到了“引用”這個概念,在此我們不妨多瞭解一些引用的知識,在 Java 中有四種引用類型,分別爲:

強引用(Strong Reference):如Object obj = new Object(),這類引用是 Java 程序中最普遍的。只要強引用還存在,垃圾收集器就永遠不會回收掉被引用的對象。

軟引用(Soft Reference):它用來描述一些可能還有用,但並非必須的對象。在系統內存不夠用時,這類引用關聯的對象將被垃圾收集器回收。JDK1.2 之後提供了SoftReference類來實現軟引用。

弱引用(Weak Reference):它也是用來描述非必須對象的,但它的強度比軟引用更弱些,被弱引用關聯的對象只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象。在 JDK1.2 之後,提供了WeakReference類來實現弱引用。

虛引用(Phantom Reference):也稱爲幻引用,最弱的一種引用關係,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的唯一目的是希望能在這個對象被收集器回收時收到一個系統通知。JDK1.2 之後提供了PhantomReference類來實現虛引用。

垃圾回收

通過上面的介紹,我們已經知道了什麼是垃圾以及如何判斷一個對象是否是垃圾。那麼接下來,我們就來了解如何回收垃圾,這就是垃圾回收算法和垃圾回收器需要做的事情了。

垃圾回收算法

標記-清除算法

標記-清除(Tracing Collector)算法是最基礎的收集算法,爲了解決引用計數法的問題而提出。它使用了根集的概念,它分爲“標記”和“清除”兩個階段:首先標記出所需回收的對象,在標記完成後統一回收掉所有被標記的對象,它的標記過程其實就是前面的可達性分析法中判定垃圾對象的標記過程。

優點:不需要進行對象的移動,並且僅對不存活的對象進行處理,在存活對象比較多的情況下極爲高效。

缺點:標記和清除過程的效率都不高,這種方法需要使用一個空閒列表來記錄所有的空閒區域以及大小,對空閒列表的管理會增加分配對象時的工作量;標記清除後會產生大量不連續的內存碎片,雖然空閒區域的大小是足夠的,但卻可能沒有一個單一區域能夠滿足這次分配所需的大小,因此本次分配還是會失敗,不得不觸發另一次垃圾收集動作。

下圖爲“標記-清除”算法的示意圖:

深入理解 JVM 垃圾回收機制及其實現原理

 

下圖爲使用“標記-清除”算法回收前後的狀態:

深入理解 JVM 垃圾回收機制及其實現原理

 

標記-整理算法

標記-整理(Compacting Collector)算法標記的過程與“標記-清除”算法中的標記過程一樣,但對標記後出的垃圾對象的處理情況有所不同,它不是直接對可回收對象進行清理,而是讓所有的對象都向一端移動,然後直接清理掉端邊界以外的內存。在基於“標記-整理”算法的收集器的實現中,一般增加句柄和句柄表。

優點:經過整理之後,新對象的分配只需要通過指針碰撞便能完成,比較簡單;使用這種方法,空閒區域的位置是始終可知的,也不會再有碎片的問題了。

缺點:GC 暫停的時間會增長,因爲你需要將所有的對象都拷貝到一個新的地方,還得更新它們的引用地址。

下圖爲“標記-整理”算法的示意圖:

深入理解 JVM 垃圾回收機制及其實現原理

 

下圖爲使用“標記-整理”算法回收前後的狀態:

深入理解 JVM 垃圾回收機制及其實現原理

 

複製算法

複製(Copying Collector)算法的提出是爲了克服句柄的開銷和解決堆碎片的垃圾回收。它將內存按容量分爲大小相等的兩塊,每次只使用其中的一塊(對象面),當這一塊的內存用完了,就將還存活着的對象複製到另外一塊內存上面(空閒面),然後再把已使用過的內存空間一次清理掉。

複製算法比較適合於新生代(短生存期的對象),在老年代(長生存期的對象)中,對象存活率比較高,如果執行較多的複製操作,效率將會變低,所以老年代一般會選用其他算法,如“標記-整理”算法。一種典型的基於複製算法的垃圾回收是stop-and-copy算法,它將堆分成對象區和空閒區,在對象區與空閒區的切換過程中,程序暫停執行。

優點:標記階段和複製階段可以同時進行;每次只對一塊內存進行回收,運行高效;只需移動棧頂指針,按順序分配內存即可,實現簡單;內存回收時不用考慮內存碎片的出現。

缺點:需要一塊能容納下所有存活對象的額外的內存空間。因此,可一次性分配的最大內存縮小了一半。

下圖爲複製算法的示意圖:

深入理解 JVM 垃圾回收機制及其實現原理

 

下圖爲使用複製算法回收前後的狀態:

深入理解 JVM 垃圾回收機制及其實現原理

 

分代收集算法

分代收集(Generational Collector)算法的將堆內存劃分爲新生代、老年代和永久代。新生代又被進一步劃分爲 Eden 和 Survivor 區,其中 Survivor 由 FromSpace(Survivor0)和 ToSpace(Survivor1)組成。所有通過new創建的對象的內存都在堆中分配,其大小可以通過-Xmx和-Xms來控制。分代收集,是基於這樣一個事實:不同的對象的生命週期是不一樣的。因此,可以將不同生命週期的對象分代,不同的代採取不同的回收算法進行垃圾回收,以便提高回收效率。

深入理解 JVM 垃圾回收機制及其實現原理

 

新生代(Young Generation):幾乎所有新生成的對象首先都是放在年輕代的。新生代內存按照 8:1:1 的比例分爲一個 Eden 區和兩個 Survivor(Survivor0,Survivor1)區。大部分對象在 Eden 區中生成。當新對象生成,Eden 空間申請失敗(因爲空間不足等),則會發起一次 GC(Scavenge GC)。回收時先將 Eden 區存活對象複製到一個 Survivor0 區,然後清空 Eden 區,當這個 Survivor0 區也存放滿了時,則將 Eden 區和 Survivor0 區存活對象複製到另一個 Survivor1 區,然後清空 Eden 和這個 Survivor0 區,此時 Survivor0 區是空的,然後將 Survivor0 區和 Survivor1 區交換,即保持 Survivor1 區爲空, 如此往復。當 Survivor1 區不足以存放 Eden 和 Survivor0 的存活對象時,就將存活對象直接存放到老年代。當對象在 Survivor 區躲過一次 GC 的話,其對象年齡便會加 1,默認情況下,如果對象年齡達到 15 歲,就會移動到老年代中。若是老年代也滿了就會觸發一次 Full GC,也就是新生代、老年代都進行回收。新生代大小可以由-Xmn來控制,也可以用-XX:SurvivorRatio來控制 Eden 和 Survivor 的比例。

老年代(Old Generation):在新生代中經歷了 N 次垃圾回收後仍然存活的對象,就會被放到年老代中。因此,可以認爲年老代中存放的都是一些生命週期較長的對象。內存比新生代也大很多(大概比例是 1:2),當老年代內存滿時觸發 Major GC 即 Full GC,Full GC 發生頻率比較低,老年代對象存活時間比較長,存活率高。一般來說,大對象會被直接分配到老年代。所謂的大對象是指需要大量連續存儲空間的對象,最常見的一種大對象就是大數組。當然分配的規則並不是百分之百固定的,這要取決於當前使用的是哪種垃圾收集器組合和 JVM 的相關參數。

永久代(Permanent Generation):用於存放靜態文件(class類、方法)和常量等。永久代對垃圾回收沒有顯著影響,但是有些應用可能動態生成或者調用一些class,例如 Hibernate 等,在這種時候需要設置一個比較大的持久代空間來存放這些運行過程中新增的類。對永久代的回收主要回收兩部分內容:廢棄常量和無用的類。永久代在 Java SE8 特性中已經被移除了,取而代之的是元空間(MetaSpace),因此也不會再出現java.lang.OutOfMemoryError: PermGen error的錯誤了。

特別地,在分代收集算法中,對象的存儲具有以下特點:

  1. 對象優先在 Eden 區分配。
  2. 大對象直接進入老年代。
  3. 長期存活的對象將進入老年代,默認爲 15 歲。

對於晉升老年代的分代年齡閾值,我們可以通過-XX:MaxTenuringThreshold參數進行控制。在這裏,不知道大家有沒有對這個默認的 15 歲分代年齡產生過疑惑,爲什麼不是 16 或者 17 呢?實際上,HotSpot 虛擬機的對象頭其中一部分用於存儲對象自身的運行時數據,如哈希碼、GC 分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程 ID、偏向時間戳等,這部分數據的長度在 32 位和 64 位的虛擬機(未開啓壓縮指針)中分別爲 32bit 和 64bit,官方稱它爲Mark word。

例如,在 32 位的 HotSpot 虛擬機中,如果對象處於未被鎖定的狀態下,那麼Mark Word的 32bit 空間中 25bit 用於存儲對象哈希碼,4bit 用於存儲對象分代年齡,2bit 用於存儲鎖標誌位,1bit 固定爲 0,其中對象的分代年齡佔 4 位,也就是從0000到1111,而其值最大爲 15,所以分代年齡也就不可能超過 15 這個數值了。

除此之外,我們再來簡單瞭解一下 GC 的分類:

新生代 GC(Minor GC / Scavenge GC):發生在新生代的垃圾收集動作。因爲 Java 對象大多都具有朝生夕滅的特性,因此 Minor GC 非常頻繁(不一定等 Eden 區滿了才觸發),一般回收速度也比較快。在新生代中,每次垃圾收集時都會發現有大量對象死去,只有少量存活,因此可選用複製算法來完成收集。

老年代 GC(Major GC / Full GC):發生在老年代的垃圾回收動作。Major GC 經常會伴隨至少一次 Minor GC。由於老年代中的對象生命週期比較長,因此 Major GC 並不頻繁,一般都是等待老年代滿了後才進行 Full GC,而且其速度一般會比 Minor GC 慢10倍以上。另外,如果分配了 Direct Memory,在老年代中進行 Full GC 時,會順便清理掉 Direct Memory 中的廢棄對象。而老年代中因爲對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清除”算法或“標記-整理”算法來進行回收。

新生代採用空閒指針的方式來控制 GC 觸發,指針保持最後一個分配的對象在新生代區間的位置,當有新的對象要分配內存時,用於檢查空間是否足夠,不夠就觸發 GC。當連續分配對象時,對象會逐漸從 Eden 到 Survivor,最後到老年代。

再多說一句,在某些場景下,老年代的對象可能引用新生代的對象,那標記存活對象的時候,需要掃描老年代中的所有對象。因爲該對象擁有對新生代對象的引用,那麼這個引用也會被稱爲GC Roots。那是不是要做全堆掃描呢?成本也太高了吧?

HotSpot 給出的解決方案是一項叫做卡表(Card Table)的技術,該技術將整個堆劃分爲一個個大小爲 512 字節的卡,並且維護一個卡表,用來存儲每張卡的一個標識位。這個標識位代表對應的卡是否可能存在指向新生代對象的引用。如果可能存在,那麼我們就認爲這張卡是髒的。

在進行 Minor GC 的時候,我們便可以不用掃描整個老年代,而是在卡表中尋找髒卡,並將髒卡中的對象加入到 Minor GC 的GC Roots裏。當完成所有髒卡的掃描之後,Java 虛擬機便會將所有髒卡的標識位清零。

想要保證每個可能有指向新生代對象引用的卡都被標記爲髒卡,那麼 Java 虛擬機需要截獲每個引用型實例變量的寫操作,並作出對應的寫標識位操作。

卡表能用於減少老年代的全堆空間掃描,這能很大的提升 GC 效率。

垃圾回收器

垃圾回收(GC)線程與應用線程保持相對獨立,當系統需要執行垃圾回收任務時,先停止工作線程,然後命令 GC 線程工作。以串行模式工作的收集器,稱爲Serial Collector,即串行收集器;與之相對的是以並行模式工作的收集器,稱爲Paraller Collector,即並行收集器。

Serial 收集器

串行收集器採用單線程方式進行收集,且在 GC 線程工作時,系統不允許應用線程打擾。此時,應用程序進入暫停狀態,即 Stop-the-world。Stop-the-world 暫停時間的長短,是衡量一款收集器性能高低的重要指標。Serial 是針對新生代的垃圾回收器,採用“複製”算法。

ParNew 收集器

並行收集器充分利用了多處理器的優勢,採用多個 GC 線程並行收集。可想而知,多條 GC 線程執行顯然比只使用一條 GC 線程執行的效率更高。一般來說,與串行收集器相比,在多處理器環境下工作的並行收集器能夠極大地縮短 Stop-the-world 時間。ParNew 是針對新生代的垃圾回收器,採用“複製”算法,可以看成是 Serial 的多線程版本

Parallel Scavenge 收集器

Parallel Scavenge 是針對新生代的垃圾回收器,採用“複製”算法,和 ParNew 類似,但更注重吞吐率。在 ParNew 的基礎上演化而來的 Parallel Scanvenge 收集器被譽爲“吞吐量優先”收集器。吞吐量就是 CPU 用於運行用戶代碼的時間與 CPU 總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)。如虛擬機總運行了 100 分鐘,其中垃圾收集花掉 1 分鐘,那吞吐量就是99%。

Parallel Scanvenge 收集器在 ParNew 的基礎上提供了一組參數,用於配置期望的收集時間或吞吐量,然後以此爲目標進行收集。通過 VM 選項可以控制吞吐量的大致範圍:

  1. -XX:MaxGCPauseMills:期望收集時間上限,用來控制收集對應用程序停頓的影響。
  2. -XX:GCTimeRatio:期望的 GC 時間佔總時間的比例,用來控制吞吐量。
  3. -XX:UseAdaptiveSizePolicy:自動分代大小調節策略。

但要注意停頓時間與吞吐量這兩個目標是相悖的,降低停頓時間的同時也會引起吞吐的降低。因此需要將目標控制在一個合理的範圍中。

Serial Old 收集器

Serial Old 是 Serial 收集器的老年代版本,單線程收集器,採用“標記-整理”算法。這個收集器的主要意義也是在於給 Client 模式下的虛擬機使用。

Parallel Old 收集器

Parallel Old 是 Parallel Scanvenge 收集器的老年代版本,多線程收集器,採用“標記-整理”算法。

CMS收集器

CMS(Concurrent Mark Swee)收集器是一種以獲取最短回收停頓時間爲目標的收集器。CMS 收集器僅作用於老年代的收集,採用“標記-清除”算法,它的運作過程分爲 4 個步驟:

  1. 初始標記(CMS initial mark)
  2. 併發標記(CMS concurrent mark)
  3. 重新標記(CMS remark)
  4. 併發清除(CMS concurrent sweep)

其中,初始標記、重新標記這兩個步驟仍然需要 Stop-the-world。初始標記僅僅只是標記一下GC Roots能直接關聯到的對象,速度很快,併發標記階段就是進行GC Roots Tracing的過程,而重新標記階段則是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始階段稍長一些,但遠比並發標記的時間短。

CMS 以流水線方式拆分了收集週期,將耗時長的操作單元保持與應用線程併發執行。只將那些必需 STW 才能執行的操作單元單獨拎出來,控制這些單元在恰當的時機運行,並能保證僅需短暫的時間就可以完成。這樣,在整個收集週期內,只有兩次短暫的暫停(初始標記和重新標記),達到了近似併發的目的。

CMS 收集器優點:併發收集,低停頓。

CMS 收集器缺點:

CMS 收集器對 CPU 資源非常敏感;

CMS 收集器無法處理浮動垃圾;

CMS 收集器是基於“標記-清除”算法,該算法的缺點都有。

CMS 收集器之所以能夠做到併發,根本原因在於採用基於“標記-清除”的算法並對算法過程進行了細粒度的分解。前面已經介紹過“標記-清除”算法將產生大量的內存碎片這對新生代來說是難以接受的,因此新生代的收集器並未提供 CMS 版本。

G1 收集器

G1(Garbage First)重新定義了堆空間,打破了原有的分代模型,將堆劃分爲一個個區域。這麼做的目的是在進行收集時不必在全堆範圍內進行,這是它最顯著的特點。區域劃分的好處就是帶來了停頓時間可預測的收集模型:用戶可以指定收集操作在多長時間內完成,即 G1 提供了接近實時的收集特性。G1 與 CMS 的特徵對比如下:

特徵 G1 CMS

併發和分代 是 是

最大化釋放堆內存 是 否

低延時 是 是

吞吐量 高 低

可預測性 強 弱

新生代和老年代的物理隔離 否 是

使用範圍 新生代和老年代 老年代

G1 具備如下特點:

並行與併發:G1 能充分利用多 CPU、多核環境下的硬件優勢,使用多個 CPU 來縮短 Stop-the-world 停頓的時間,部分其他收集器原來需要停頓 Java 線程執行的 GC 操作,G1 收集器仍然可以通過併發的方式讓 Java 程序繼續運行。

分代收集:打破了原有的分代模型,將堆劃分爲一個個區域。

空間整合:與 CMS 的“標記-清除”算法不同,G1 從整體來看是基於“標記-整理”算法實現的收集器,從局部(兩個 Region 之間)上來看是基於“複製”算法實現的。但無論如何,這兩種算法都意味着 G1 運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一次 GC。

可預測的停頓:這是 G1 相對於 CMS 的一個優勢,降低停頓時間是 G1 和 CMS 共同的關注點。

在 G1 之前的其他收集器進行收集的範圍都是整個新生代或者老年代,而 G1 不再是這樣。在堆的結構設計時,G1 打破了以往將收集範圍固定在新生代或老年代的模式,G1 將堆分成許多相同大小的區域單元,每個單元稱爲 Region,Region 是一塊地址連續的內存空間,G1 模塊的組成如下圖所示:

深入理解 JVM 垃圾回收機制及其實現原理

 

堆內存會被切分成爲很多個固定大小的 Region,每個是連續範圍的虛擬內存。堆內存中一個 Region 的大小可以通過-XX:G1HeapRegionSize參數指定,其區間最小爲 1M、最大爲 32M,默認把堆內存按照 2048 份均分。

每個 Region 被標記了 E、S、O 和 H,這些區域在邏輯上被映射爲 Eden,Survivor 和老年代。存活的對象從一個區域轉移(即複製或移動)到另一個區域,區域被設計爲並行收集垃圾,可能會暫停所有應用線程。

如上圖所示,區域可以分配到 Eden,Survivor 和老年代。此外,還有第四種類型,被稱爲巨型區域(Humongous Region)。Humongous 區域是爲了那些存儲超過 50% 標準 Region 大小的對象而設計的,它用來專門存放巨型對象。如果一個 H 區裝不下一個巨型對象,那麼 G1 會尋找連續的 H 分區來存儲。爲了能找到連續的 H 區,有時候不得不啓動 Full GC。

G1 收集器之所以能建立可預測的停頓時間模型,是因爲它可以有計劃地避免在整個 Java 堆中進行全區域的垃圾收集。G1 會通過一個合理的計算模型,計算出每個 Region 的收集成本並量化,這樣一來,收集器在給定了“停頓”時間限制的情況下,總是能選擇一組恰當的 Region 作爲收集目標,讓其收集開銷滿足這個限制條件,以此達到實時收集的目的。

對於打算從 CMS 或者 ParallelOld 收集器遷移過來的應用,按照官方的建議,如果發現符合如下特徵,可以考慮更換成 G1 收集器以追求更佳性能:

1.實時數據佔用了超過半數的堆空間;

2.對象分配率或“晉升”的速度變化明顯;

3.期望消除耗時較長的GC或停頓(超過 0.5 ~ 1 秒)。

G1 手機的運作過程大致如下:

初始標記(Initial Marking):僅僅只是標記一下GC Roots能直接關聯到的對象,並且修改 TAMS(Next Top at Mark Start)的值,讓下一階段用戶程序併發運行時,能在正確可用的 Region 中創建新對象,這階段需要停頓線程,但耗時很短。

併發標記(Concurrent Marking):是從GC Roots開始堆中對象進行可達性分析,找出存活的對象,這階段耗時較長,但可與用戶程序併發執行。

最終標記(Final Marking):是爲了修正併發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分標記記錄,虛擬機將這段時間對象變化記錄在線程 Remembered Set Logs 裏面,最終標記階段需要把 Remembered Set Logs 的數據合併到 Remembered Set 中,這階段需要停頓線程,但是可並行執行。

篩選回收(Live Data Counting and Evacuation):首先對各個 Region 的回收價值和成本進行排序,根據用戶所期望的 GC 停頓時間來制定回收計劃。這個階段也可以做到與用戶程序一起併發執行,但是因爲只回收一部分 Region,時間是用戶可控制的,而且停頓用戶線程將大幅提高收集效率。

G1 的 GC 模式可以分爲兩種,分別爲:

Young GC:在分配一般對象(非巨型對象)時,當所有 Eden 區域使用達到最大閥值並且無法申請足夠內存時,會觸發一次 YoungGC。每次 Young GC 會回收所有 Eden 以及 Survivor 區,並且將存活對象複製到 Old 區以及另一部分的 Survivor 區。

Mixed GC:當越來越多的對象晉升到老年代時,爲了避免堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即 Mixed GC,該算法並不是一個 Old GC,除了回收整個新生代,還會回收一部分的老年代,這裏需要注意:是一部分老年代,而不是全部老年代,可以選擇哪些 Old 區域進行收集,從而可以對垃圾回收的耗時時間進行控制。G1 沒有 Full GC概念,需要 Full GC 時,調用 Serial Old GC 進行全堆掃描。

查看 JVM 使用的默認垃圾收集器

在 Mac 終端或者 Windows 的 CMD 執行如下命令:

java -XX:+PrintCommandLineFlags -version

以我的電腦爲例,執行結果爲:

深入理解 JVM 垃圾回收機制及其實現原理

 

在此,給出垃圾收集相關的常用參數及其含義:

參數 含義

UseSerialGC 虛擬機運行在 Client 模式下的默認值,

打開此開關後,使用Serial + Serial Old的收集器組合進行內存回收

UseParNewGC 打開此開關後,使用ParNew + Serial Old的收集器組合進行內存回收

UseConcMarkSweepGC 打開此開關後,使用ParNew + CMS + Serial Old的收集器組合

進行內存回收,Serial Old收集器將作爲 CMS 收集器出現Concurrent Mode Failure失敗後的備用收集器使用

UseParallelGC 虛擬機運行在 Server 模式下的默認值,打開此開關後,

使用Parallel Scavenge + Serial Old(PS MarkSweep)

的收集器組合進行內存回收

UseParallelOldGC 打開此開關後,使用Parallel Scavenge + Parallel Old的收集器 組合進行內存回收

由此可知,JDK 8 默認打開了UseParallelGC參數,因此使用了Parallel Scavenge + Serial Old的收集器組合進行內存回收。

到這裏,關於 JVM 垃圾回收機制及其實現原理,我們就講完了,希望能夠對大家有所幫助!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章