JVM-垃圾回收篇

JVM-垃圾回收篇

前言

C語言中的記憶體需要程式設計師手動進行申請和回收,不然會出現記憶體泄露的問題。

舉個例子

下圖是單鏈表操作中的記憶體申請和回收

  1. C語言版本(參考程式碼鏈接:<//gitee.com/qianlilo/linklist)
    1. 插入單鏈表時,進行記憶體空間的申請,需要指明記憶體空間的大小

  1. 刪除單鏈表中某一個節點時,對該節點的記憶體空間進行free操作,釋放空間。(s是垃圾)

  1. Java版本(參考程式碼鏈接:<//gitee.com/moline-x/LinkListReview)

    1. 插入單鏈表時,給前一個節點增加要插入節點的引用,並且將前一個節點的原先的下一個節點賦給插入節點的next指針。(不涉及到記憶體大小的申請,只設計到new對象操作)

    2. 刪除單鏈表時,將前一個節點的指針指向下一個節點的指針,並且將要刪除的指針的next賦值null操作。其中的retNode是待回收的垃圾。

      (只是將不會用到的對象其引用去掉,不涉及記憶體的釋放操作)

JVM 有哪些垃圾回收演算法?

  • 標記-清除演算法:標記無用對象,然後進行清除回收。缺點:效率不高,無法清除垃圾碎片。
  • 複製演算法:按照容量劃分二個大小相等的記憶體區域,當一塊用完的時候將活著的對象複製到另一塊上,然後再把已使用的記憶體空間一次清理掉。缺點:記憶體使用率不高,只有原來的一半。
  • 標記-整理演算法:標記無用對象,讓所有存活的對象都向一端移動,然後直接清除掉端邊界以外的記憶體。
  • 分代演算法:根據對象存活周期的不同將記憶體劃分為幾塊,一般是新生代和老年代,新生代基本採用複製演算法,老年代採用標記整理演算法。

標記-清除演算法

標記無用對象,然後進行清除回收。

標記-清除演算法(Mark-Sweep)是一種常見的基礎垃圾收集演算法,它將垃圾收集分為兩個階段:

  • 標記階段:標記出可以回收的對象。
  • 清除階段:回收被標記的對象所佔用的空間。

標記-清除演算法之所以是基礎的,是因為後面講到的垃圾收集演算法都是在此演算法的基礎上進行改進的。

優點:實現簡單,不需要對象進行移動。

缺點:標記、清除過程效率低,產生大量不連續的記憶體碎片,提高了垃圾回收的頻率。

標記-清除演算法的執行的過程如下圖所示

img

複製演算法

為了解決標記-清除演算法的效率不高的問題,產生了複製演算法。它把記憶體空間劃為兩個相等的區域,每次只使用其中一個區域。垃圾收集時,遍歷當前使用的區域,把存活對象複製到另外一個區域中,最後將當前使用的區域的可回收的對象進行回收。

優點:按順序分配記憶體即可,實現簡單、運行高效,不用考慮記憶體碎片。

缺點:可用的記憶體大小縮小為原來的一半,對象存活率高時會頻繁進行複製。

複製演算法的執行過程如下圖所示

img

標記-整理演算法

Mark-Compact,又稱標記-壓縮演算法

在新生代中可以使用複製演算法,但是在老年代就不能選擇複製演算法了,因為老年代的對象存活率會較高,這樣會有較多的複製操作,導致效率變低。標記-清除演算法可以應用在老年代中,但是它效率不高,在記憶體回收後容易產生大量記憶體碎片。因此就出現了一種標記-整理演算法(Mark-Compact)演算法,與標記-整理演算法不同的是,在標記可回收的對象後將所有存活的對象壓縮到記憶體的一端,使他們緊湊的排列在一起,然後對端邊界以外的記憶體進行回收。回收後,已用和未用的記憶體都各自一邊。

優點:解決了標記-清理演算法存在的記憶體碎片問題。

缺點:仍需要進行局部對象移動,一定程度上降低了效率。

標記-整理演算法的執行過程如下圖所示

img

分代收集演算法

當前商業虛擬機都採用分代收集的垃圾收集演算法。分代收集演算法,顧名思義是根據對象的存活周期將記憶體劃分為幾塊。一般包括年輕代老年代永久代,如圖所示:

img

  • 伊甸園(Eden):這是對象最初誕生的區域,並且對大多數對象來說,這裡是它們唯一存在過的區域。
  • 倖存者樂園(Survivor):從伊甸園倖存下來的對象會被挪到這裡。
  • 終身頤養園(Tenured):這是足夠老的倖存對象的歸宿。年輕代收集(Minor-GC)過程是不會觸及這個地方的。當年輕代收集不能把對象放進終身頤養園時,就會觸發一次完全收集(Major-GC),這裡可能還會牽扯到壓縮,以便為大對象騰出足夠的空間。
  • 永久代:通常是指方法區。

JVM 有哪些垃圾回收器?

概述

如果說垃圾收集演算法是記憶體回收的方法論,那麼垃圾收集器就是記憶體回收的具體實現。下圖展示了7種作用於不同分代的收集器,其中用於回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,還有用於回收整個Java堆的G1收集器。不同收集器之間的連線表示它們可以搭配使用。

img

  • Serial收集器(複製演算法): 新生代單執行緒收集器,標記和清理都是單執行緒,優點是簡單高效;
  • ParNew收集器 (複製演算法): 新生代收並行集器,實際上是Serial收集器的多執行緒版本,在多核CPU環境下有著比Serial更好的表現;
  • Parallel Scavenge(並行清除)收集器 (複製演算法): 新生代並行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用戶執行緒時間/(用戶執行緒時間+GC執行緒時間),高吞吐量可以高效率的利用CPU時間,儘快完成程式的運算任務,適合後台應用等對交互相應要求不高的場景;
  • Serial Old收集器 (標記-整理演算法): 老年代單執行緒收集器,Serial收集器的老年代版本;
  • Parallel Old收集器 (標記-整理演算法): 老年代並行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;
  • CMS(Concurrent Mark Sweep)收集器(標記-清除演算法): 老年代並行收集器,以獲取最短回收停頓時間為目標的收集器,具有高並發、低停頓的特點,追求最短GC回收停頓時間。
  • G1(Garbage First)收集器 (標記-整理演算法): Java堆並行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基於「標記-整理」演算法實現,也就是說不會產生記憶體碎片。此外,G1收集器不同於之前的收集器的一個重要特點是:G1回收的範圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的範圍僅限於新生代或老年代。

先看下圖解HotSpot虛擬機所包含的收集器:

img

圖中展示了7種作用於不同分代的收集器,如果兩個收集器之間存在連線,則說明它們可以搭配使用。虛擬機所處的區域則表示它是屬於新生代還是老年代收集器。

新生代收集器:Serial、ParNew、Parallel Scavenge

老年代收集器:CMS、Serial Old、Parallel Old

整堆收集器: G1

幾個相關概念

垃圾是什麼

並行收集:指多條垃圾收集執行緒並行工作,但此時用戶執行緒仍處於等待狀態。

並發收集:指用戶執行緒與垃圾收集執行緒同時工作(不一定是並行的可能會交替執行)。用戶程式在繼續運行,而垃圾收集程式運行在另一個CPU上。

吞吐量:即CPU用於運行用戶程式碼的時間與CPU總消耗時間的比值(吞吐量 = 運行用戶程式碼時間 / ( 運行用戶程式碼時間 + 垃圾收集時間 ))。例如:虛擬機共運行100分鐘,垃圾收集器花掉1分鐘,那麼吞吐量就是99%

經典的垃圾回收器

一:Serial 收集器

Serial收集器是最基本的、發展歷史最悠久的收集器。

特點:單執行緒、簡單高效(與其他收集器的單執行緒相比),對於限定單個CPU的環境來說,Serial收集器由於沒有執行緒交互的開銷,專心做垃圾收集自然可以獲得最高的單執行緒手機效率。收集器進行垃圾回收時,必須暫停其他所有的工作執行緒,直到它結束(Stop The World)。

應用場景:適用於Client模式下的虛擬機。

Serial / Serial Old收集器運行示意圖

img

二:ParNew收集器

ParNew收集器其實就是Serial收集器的多執行緒版本

除了使用多執行緒外其餘行為均和Serial收集器一模一樣(參數控制、收集演算法、Stop The World、對象分配規則、回收策略等)。

特點:多執行緒、ParNew收集器默認開啟的收集執行緒數與CPU的數量相同,在CPU非常多的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的執行緒數。

   和Serial收集器一樣存在Stop The World問題

應用場景:ParNew收集器是許多運行在Server模式下的虛擬機中首選的新生代收集器,因為它是除了Serial收集器外,唯一一個能與CMS收集器配合工作的。

ParNew/Serial Old組合收集器運行示意圖如下:

img

三:Parallel Scavenge 收集器

與吞吐量關係密切,故也稱為吞吐量優先收集器

特點:屬於新生代收集器也是採用複製演算法的收集器,又是並行的多執行緒收集器(與ParNew收集器類似)。

該收集器的目標是達到一個可控制的吞吐量。還有一個值得關注的點是:GC自適應調節策略(與ParNew收集器最重要的一個區別)

GC自適應調節策略:Parallel Scavenge收集器可設置-XX:+UseAdptiveSizePolicy參數。當開關打開時不需要手動指定新生代的大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRation)、晉陞老年代的對象年齡(-XX:PretenureSizeThreshold)等,虛擬機會根據系統的運行狀況收集性能監控資訊,動態設置這些參數以提供最優的停頓時間和最高的吞吐量,這種調節方式稱為GC的自適應調節策略。

Parallel Scavenge收集器使用兩個參數控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停頓時間
  • XX:GCRatio 直接設置吞吐量的大小。

四:Serial Old 收集器

Serial Old是Serial收集器的老年代版本。

特點:同樣是單執行緒收集器,採用標記-整理演算法。

應用場景:主要也是使用在Client模式下的虛擬機中。也可在Server模式下使用。

Server模式下主要的兩大用途(在後續中詳細講解···):

  1. 在JDK1.5以及以前的版本中與Parallel Scavenge收集器搭配使用。
  2. 作為CMS收集器的後備方案,在並發收集Concurent Mode Failure時使用。

Serial / Serial Old收集器工作過程圖(Serial收集器圖示相同):

img

五:Parallel Old 收集器

是Parallel Scavenge收集器的老年代版本。

特點多執行緒,採用標記-整理演算法。

應用場景:注重高吞吐量以及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old 收集器。

Parallel Scavenge/Parallel Old收集器工作過程圖:

img

六:CMS收集器

前言:一種以獲取最短回收停頓時間為目標的收集器。

CMS 是英文 Concurrent Mark-Sweep 的簡稱,是以犧牲吞吐量為代價來獲得最短回收停頓時間的垃圾回收器。對於要求伺服器響應速度的應用上,這種垃圾回收器非常適合。在啟動 JVM 的參數加上「-XX:+UseConcMarkSweepGC」來指定使用 CMS 垃圾回收器。

CMS 使用的是標記-清除的演算法實現的,所以在 gc 的時候回產生大量的記憶體碎片,當剩餘記憶體不能滿足程式運行要求時,系統將會出現 Concurrent Mode Failure,臨時 CMS 會採用 Serial Old 回收器進行垃圾清除,此時的性能將會被降低。

特點:基於標記-清除演算法實現。並發收集、低停頓。

應用場景:適用於注重服務的響應速度,希望系統停頓時間最短,給用戶帶來更好的體驗等場景下。如web程式、b/s服務。

CMS收集器的運行過程分為下列4步:

初始標記:標記GC Roots能直接到的對象(原因:就像遍歷樹形結構,第二層開始往葉子節點遍歷是可以並行的,這樣停頓時間最短)。速度很快但是仍存在Stop The World問題。

並發標記:進行GC Roots Tracing 的過程,找出存活對象且用戶執行緒可並發執行。

重新標記:為了修正並發標記期間因用戶程式繼續運行而導致標記產生變動的那一部分對象的標記記錄。仍然存在Stop The World問題。時間比初始標記稍微長一些,但是遠比並發標記的時間短。

並發清除:對標記的對象進行清除回收。

CMS收集器的記憶體回收過程是與用戶執行緒一起並發執行的。

CMS收集器的工作過程圖:

img

優點

  1. 並發收集
  2. 低遲延

缺點

  1. 對CPU資源非常敏感。在並發階段,雖然用戶進程不會停頓,但是GC佔用了一部分的執行緒導致應用程式變慢,總吞吐量會變慢。

  2. 無法處理浮動垃圾(在並發標記中新產生的垃圾對象),可能出現Concurrent Model Failure失敗而導致另一次Full GC的產生。

  3. 因為採用標記-清除演算法所以會存在空間碎片的問題,導致大對象無法分配空間,不得不提前觸發一次Full GC。

問題

CMS在JDK後續版本中的地位

七:G1收集器

前言

  1. 一款面向服務端應用的垃圾收集器。
  2. JDK9之後的默認來及回收器
  3. 軟實時 soft real-time
  4. 以空間換時間
  5. 簡化JVM性能調優

特點如下:

  1. 並行與並發:G1能充分利用多CPU、多核環境下的硬體優勢,使用多個CPU來縮短Stop-The-World停頓時間。部分收集器原本需要停頓Java執行緒來執行GC動作,G1收集器仍然可以通過並發的方式讓Java程式繼續運行。在乎吞吐量,就改為並行的

    1. 並行可以多個GC執行緒同時工作,但是用戶執行緒STW
    2. 並發時,GC執行緒可以和用戶執行緒交替進行執行。
  2. 分代收集:G1能夠獨自管理整個Java堆,並且採用不同的方式去處理新創建的對象和已經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。不要求堆是連續的

  3. 空間整合:【標記-整理演算法】G1運作期間不會產生空間碎片,收集後能提供規整的可用記憶體。

  4. 可預測的停頓:G1除了追求低停頓外,還能建立可預測的停頓時間模型。能讓使用者明確指定在一個長度為M毫秒的時間段內,消耗在垃圾收集上的時間不得超過N毫秒。

G1為什麼能建立可預測的停頓時間模型?

因為它有計劃的避免在整個Java堆中進行全區域的垃圾收集。G1跟蹤各個Region裡面的垃圾堆積的大小,在後台維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。這樣就保證了在有限的時間內可以獲取儘可能高的收集效率。

G1與其他收集器的區別

其他收集器的工作範圍是整個新生代或者老年代、G1收集器的工作範圍是整個Java堆。在使用G1收集器時,它將整個Java堆劃分為多個大小相等的獨立區域(Region)。雖然也保留了新生代、老年代的概念,但新生代和老年代不再是相互隔離的,他們都是一部分Region(不需要連續)的集合。

G1收集器存在的問題

Region不可能是孤立的,分配在Region中的對象可以與Java堆中的任意對象發生引用關係。在採用可達性分析演算法來判斷對象是否存活時,得掃描整個Java堆才能保證準確性。其他收集器也存在這種問題(G1更加突出而已)。會導致Minor GC效率下降。

G1收集器是如何解決上述問題的?

採用Remembered Set來避免整堆掃描。G1中每個Region都有一個與之對應的Remembered Set,虛擬機發現程式在對Reference類型進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用對象是否處於多個Region中(即檢查老年代中是否引用了新生代中的對象),如果是,便通過CardTable把相關引用資訊記錄到被引用對象所屬的Region的Remembered Set中。當進行記憶體回收時,在GC根節點的枚舉範圍中加入Remembered Set即可保證不對全堆進行掃描也不會有遺漏。

如果不計算維護 Remembered Set 的操作,G1收集器大致可分為如下步驟:

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

並發標記:從GC Roots開始對堆中對象進行可達性分析,找出存活對象。(耗時較長,但可與用戶程式並發執行)

最終標記:為了修正在並發標記期間因用戶程式執行而導致標記產生變化的那一部分標記記錄。且對象的變化記錄在執行緒Remembered Set Logs裡面,把Remembered Set Logs裡面的數據合併到Remembered Set中。(需要執行緒停頓,但可並行執行。)

篩選回收:對各個Region的回收價值和成本進行排序,根據用戶所期望的GC停頓時間來制定回收計劃。(可並發執行)

G1收集器運行示意圖:

img

缺點

不同點

G1怎麼工作

分區Region:化整為零(分散開)

指針碰撞

​ 假設JVM堆中記憶體是規整的,所有用過的記憶體放在一邊,沒用過的記憶體放在另一邊,中間放著一個指針作為分界點的指示器,那所分配記憶體的過程就僅僅是把那個指針向空閑空間的方向挪動一段與對象大小相等的距離,這種分配方式被稱為「指針碰撞(Bump the Pointer)」。

垃圾回收過程

新生代垃圾回收器和老年代垃圾回收器都有哪些?有什麼區別?

  • 新生代回收器:Serial、ParNew、Parallel Scavenge
  • 老年代回收器:Serial Old、Parallel Old、CMS
  • 整堆回收器:G1

新生代垃圾回收器一般採用的是複製演算法,複製演算法的優點是效率高,缺點是記憶體利用率低

老年代回收器一般採用的是標記-整理的演算法進行垃圾回收。

簡述分代垃圾回收器是怎麼工作的?

分代回收器有兩個分區:老生代和新生代,新生代默認的空間佔比總空間的 1/3,老生代的默認佔比是 2/3。

新生代使用的是複製演算法,新生代里有 3 個分區:Eden、To Survivor、From Survivor,它們的默認佔比是 8:1:1,它的執行流程如下:

  • 把 Eden + From Survivor 存活的對象放入 To Survivor 區;
  • 清空 Eden 和 From Survivor 分區;
  • From Survivor 和 To Survivor 分區交換,From Survivor 變 To Survivor,To Survivor 變 From Survivor。

每次在 From Survivor 到 To Survivor 移動時都存活的對象,年齡就 +1,當年齡到達 15(默認配置是 15)時,升級為老生代。大對象也會直接進入老生代。

老生代當空間佔用到達某個值之後就會觸發全局垃圾收回,一般使用標記整理的執行演算法。以上這些循環往複就構成了整個分代垃圾回收的整體執行流程。

總結

根據具體的情況選用不同的垃圾收集器

GC發展階段

組合

紅色虛線:JDK8棄用,JDK9移除

綠色的框,JDK9棄用,JDK14去掉,

綠色的線代表JDK14棄用。

怎麼選擇垃圾回收器

引用的分類

  • 強引用:GC時不會被回收
  • 軟引用:描述有用但不是必須的對象,在發生記憶體溢出異常之前被回收
  • 弱引用:描述有用但不是必須的對象,在下一次GC時被回收
  • 虛引用(幽靈引用/幻影引用):無法通過虛引用獲得對象,用PhantomReference實現虛引用,虛引用用來在GC時返回一個通知。

參考資料

JVM_01 簡介 – 傲嬌的大王 – 部落格園

(3條消息)JVM學習筆記(三)——記憶體管理和垃圾回收_走向架構師之路-CSDN部落格

(3條消息)Java虛擬機(JVM)面試題(2020最新版)_ThinkWon的部落格-CSDN部落格

(4條消息)Java方法區、棧及堆_蝸牛-CSDN部落格

Jvm垃圾回收器(終結篇) – 不二塵 – 部落格園

24個Jvm面試題總結及答案_ITPUB部落格

Tags: