一站式搞定JVM的垃圾回收機制(最全版)

先提提要

首先在開始進行垃圾回收之前的總結,先看一張圖片:
在這裏插入圖片描述
下面開始對其的每一個部分進行一個具體的講解:

  • 程序計數器:是一塊較小的內存單元,可以看做是當前的執行的線程所執行的字節碼的行號指示器,字節碼解釋器就是通過這個改變計數器的值來選取下一條需要執行的字節碼指令,分支,循環,跳轉等都需要依賴這個計數器來完成。
  • Java虛擬機棧來說也是一個個的線程所私有的。描述的是java方法執行的內存模型,每一個方法在執行的同時都會創建一個棧幀,用於存儲局部變量表,操作數,動態鏈接等,每一個方法在調用直到完成的過程,都對應着一個棧幀在虛擬機中從入棧到出棧的過程。
  • 本地方法棧與虛擬機棧不同的地方在於,虛擬機棧爲虛擬機執行Java方法服務(也就是字節碼)服務,但是本地方法棧則會虛擬機使用到 Native方法服務。
  • Java堆: 是虛擬所管理的內存最大的一塊,對於Java堆來說,也是被線程所共享的,在虛擬機啓動的時候,堆中存放的是所有實例化的對象,但是也是GC回收的主要對象。
  • 方法區和堆都是對於線程來說是共享的,存放的是一些已經被虛擬機加載的類的信息,常量,靜態變量,既時編譯器編譯後的代碼數據。 運行時常量池 是方法區的一部分,對於class文件來說 除了有類版本字段,方法,接口等信息來說,還有一個常量池,用於存放在編譯器生成的各種字面量與符號引用。這部分的內容將在類加載後進入方法區的運行時常量池中存放。

JVM 垃圾回收模型

前景提要: 首先對於垃圾回收器而言,我們可能也早就聽過,但是也僅僅限於聽說過的層面上。其實對於垃圾回收的時候,哪些對象需要回收?什麼時候回收? 如何回收都是問題,下面我們就開始進行具體的講解。


在完成垃圾回收之前,我們需要判斷哪些的對象是需要進行回收的–對於那些已經死去的對象我們需要進行回收。但是如何判斷對象是都已經死去,有兩種的方法。

引用計數算法(Reference Counting)

給對象添加一個引用計數器,當有一個地方引用它的時候,計數器加一。當引用失效的時候,計數器減一,任何時刻計數器爲0的對象就是不可能再被引用的。此時就可以對其進行回收處理。此方法卻不能解決對象循環引用的問題:
循環引用的栗子
開始有一個方法A和一個方法B,開始時候 有對象對A進行引用,也有一些棧方法什麼的對B進行引用,且兩則之間有相互引用的關係。後來其餘的引用不再工作,這兩個就互相引用,此時引用計數器的值也不是0,但是對於外部來說,這兩個方法以及不具有任何的價值,但是就是不能夠被回收掉,就是循環引用的問題。

根搜索算法。

在java中 使用根搜索算法判斷是否存活。
基本思路: 就是通過一系列的稱爲“GC Roots”的點作爲起始進行向下搜索,當一個對象到GC Roots 沒有任何引用鏈相連,就證明此對象是不可用的。
但是問題來了什麼是 GC Roots:

GC Roots

以下的引用我們可以稱作其爲:

  • 在VM (幀中的本地變量)中的引用
  • 方法區中的靜態引用。
  • JNI(即一般說的Native方法)中的引用

在講述完判斷是否是垃圾的方法以後,下面我們開始要做的就是 對其進行回收。

垃圾回收算法(四種)

標記清除算法(Mark-Sweep):

兩個階段: 標記- 清除
首先 標記所有需要回收的對象(就是在前面進行過判斷的“已死的對象”),然後回收所有需要回收的對象。
缺點

  • 效率都不太高
  • 會產生大量的不連續內存碎片,空間碎片太多,會導致後續使用中由於無法找到足夠的連續內存而提前觸發另一次的垃圾蒐集動作。
    GC的次數越多 碎片化情況就會越加嚴重。
    在這裏插入圖片描述

如上圖所示: 此時對於 G,F,J,M 就無法進行相對應的回收。

標記整理算法(Mark-Compact):

標記過程仍然存在,但是後續的步驟不是進行直接清理,而是讓所有存活的對象一端移動,然後直接清理掉這段邊界以外的內存。
在這裏插入圖片描述
如上圖所示,進行一次標記以後進行對應的整理操作。

複製算法(Copying):

複製算法簡單介紹

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-EpRGHyVb-1582811209567)(en-resource://database/1338:1)]

複製算法具體實現過程

在這裏插入圖片描述

這裏的複製算法在新生代中都會使用到的算法,至於什麼是新生代,後面會具體介紹

複製算法的優點。

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-NBW7xzoh-1582811209568)(en-resource://database/1342:1)]

分代算法(Generational)

分代簡介

在這裏插入圖片描述

新生代

在這裏插入圖片描述

老年代

在這裏插入圖片描述
解惑時間
爲什麼:新生代要使用複製算法。

第一個原因:我們也都知道了對於垃圾回收回收的就是堆中的內容。雖然說也會回收方法區中的空間,但是效率很低下,這裏我們不再進行考量。所以現在來說新創建的對象都是放在新生代中的,但是這些新的對象卻是朝生息滅(就例如我們創建一個對象,但是很快我們就不會再使用,就會使用一次來說)。前面我們也講到了複製算法的好處與不好的地方。 對於複製算法來說,是進行全部的複製,若是太多 效率會低 所以 就是新生代裏面 很多都是沒有用的,使用複製時候 可以被回收的不多也不會複製太多 就使用複製算法。
既然是老年代,就是經歷了很多次的垃圾回收還存在的,所以就是說還有價值的,既然價值還在 下一次的回收也不一定會回收掉,使用複製算法時候 就會複製很多 這個時候 複製成本就會很多 。
對於複製算法 就會浪費很多空間。
第二個原因: 由於複製算法會消耗空間 一個 from 一個 to 新生代操作完成以後 但是還是會有很多的存活的對象,這個時候 從 fromto時候 若是空間不夠用 可以轉到老年代裏面去,此時的老年代就起到了一個分配擔保的作用。


前面的第一節和第二節我們講到了 如何判斷一個對象是否存活(即其是不是垃圾了已經。而是如何對這些垃圾進行收集的算法),但是想要實現這些算法,我們還需要更加嚴格的限制,才能正常運行以上所說的信息。

算法的實現

枚舉根節點

前面提到了可以使用根節點來進行判斷哪些是垃圾對象,但是就是需要邏輯性得使用引用鏈來進行查找來判斷哪些的對象可以連成一個鏈,但是很多的應用僅僅方法區就有幾百兆,所以一個個的查找引用是不現實的,所以虛擬機提供了一個可以直接得知哪些地方存放着引用:使用一組稱爲OopMap的數據結構來實現的,在完成了類的加載以後,HotSop就會把對象內什麼的偏移量上是什麼類型的數據計算出來,在Jit編譯的過程中,也就會在特定的位置記錄下棧和寄存器中哪些位置是引用。這樣得到節點就會簡單很多。

安全點

有了前面 OopMap的協助之下,對於 GC Roots的枚舉就會簡單很多,但是問題也應運而生,雖然這個OopMap可以幫助我們,但是爲每一條指令都生成對應的OopMap,就需要很多的額外空間,這樣 GC的空間成本就會很高。
所以虛擬機也不會爲每條指令都會生成OopMap,而是只會在特定的位置記錄下這些信息,稱爲安全點。即程序執行時候並非在所有地方都停頓下來開始GC,而是在特定的地方纔會停止,開始GC。(這裏需要進行解釋一下,現代判斷垃圾的方法是使用根搜素算法,所以對於一個根來說纔會進行搜索,纔會開始一次的GC過程,所以可以這樣進行理解,在特定的地方停頓,在特定的地方進行根搜索)

多線程時候

雖然暫時解決了一個問題,但是還有一個問題是,如何讓多線程也都能夠跑到最近的安全點停下來呢。提供了兩個方法:
方法一: 搶先式中斷: 不需要線程的執行代碼的主動配合,而是在發生GC的時候,將所有的線程進行中斷,若是發現有的線程不再“安全點”上,就讓其恢復,繼續跑到安全點上去。對這個方法來說太過於繁瑣也開銷巨大,現在不再使用。
方法二: 主動式中斷:就是說 不會對線程直接進行操作,而是說 設置一些標誌點,各個線程在運行的過程中,去主動的輪詢這些標誌,若是發現爲真,就將自己掛起。這裏需要注意的是,輪詢點和安全點也是重合的。爲什麼要重合呢: 就是說 當要進行GC時候,輪詢點會爲真,此時線程停在這裏,也是安全點,進行根節點選定以後,就可以進行判斷哪些是垃圾。

安全區域

前面是安全點的介紹,基礎是說,線程在運行的時候,但是還有一種情況就是說不執行時候呢。所謂的不執行就是說,沒有分配CPU,典型的例子是線程出現休眠狀態,或者是說處於阻塞的狀態呢?這個時候前面提到的輪詢就不再適用。
safe Region : 安全區:是指在一段代碼片段中,引用的關係不會發生變化。在這個區域中的任何地方開始GC都是安全的。我們也可以把這段區域看做是擴展的 安全點。


前面的介紹了垃圾收集的算法,是指的是內存回收的方法論,此時的垃圾回收器就是這些算法的具體實現。可以說 分代的模型是GC的宏觀願望。垃圾回收器是GC的具體實現,每種的垃圾回收器都有自己使用的場景。

垃圾收集器

前面筆者也說到了對於現在的垃圾收集器大都是基於分代算法下面先看一張的總覽:

在這裏插入圖片描述
以上介紹了我們下面要介紹的一些垃圾收集器。

並行與併發

在介紹之前,我們先進行一個概念的瞭解:
並行:指多個垃圾回收器的線程同時工作,但是用戶線程處於等待狀態。
併發:指收集器在工作的同時,可以運行用戶線程也工作。
注: 但是併發也不能說就是解決了GC停頓的問題,在關鍵的步驟,例如在收集器標記垃圾的時候還是要停頓的,但是在清除時候 回收器線程可以與用戶線程併發執行。

Serial 收集器。

是一種單線程的收集器,在收集的時候 會暫停所有的工作線程(簡稱STW(stop the word)) 使用複製收集算法。在老年代使用 Mark-Compact。虛擬機運行在Client模式時候默認新生代收集器。

ParNew

是Serial收集器的多線程版本,除了使用多個收集線程外,其餘都是一樣的,且這種收集器是虛擬機運行在Server模式的默認新生代收集器。但是對於多CPU的環境中,這種收集器的效果不見得比Serial效果好。

Parallel Scavenge 收集器

在這裏插入圖片描述
對於用戶來說 停頓時間越短就越適合需要與用戶交互的程序,良好的響應速度能夠提升用戶體驗,對於 PS來說 高吞吐量則可以高效地利用CPU時間,儘快完成程序的運算任務,適用於在後臺運算但是交互不太多的任務。

Serial Old 收集器

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

Parallel Old 收集器

是Parallel Scavenge 收集器的老年代版本,使用多線程和“標記整理”算法、

CMS 垃圾回收器

(Concurrent Mark Sweep)CMS垃圾回收器顧名思義是一種以獲取最短回收停頓時間爲目標的收集器。就是說 想要停頓的時間更少,也是因爲很大一部分的Java應用集中在互聯網或者B/S系統的服務器上,這樣的服務器很需要響應速度,給用戶較好的體驗,其有四個步驟:

初始標記:

僅僅是標記與GC Roots能夠直接關聯到的對象,速度很快

併發標記

就是進行Gc RootsTracing的過程

重新標記

是爲了修正在併發標記期間因用戶程序的繼續運作而導致標記參數變動的哪一步分對象的標記記錄,也比並發標記的時間要短。

併發清除

在這裏插入圖片描述

缺點:

  1. 對CPU資源非常敏感,實際上面向併發設計的程序都對CPU資源比較敏感。在 併發執行的階段,雖然不會導致用戶線程停頓,但是會因爲佔用了一部分線程,從而導致應用程序變慢,總吞吐量會降低。
  2. 無法處理浮動垃圾。因爲CMS併發清理階段用戶的線程還在運行,就會導致有新的垃圾不斷產生,CMS無法在當次處理掉,只好等待以一次的GC,這一部分的垃圾叫做浮動垃圾。
  3. 是基於標記-清楚,算法。所以就會產生大量的空間碎片,就會導致雖然老年代還有很大的空間,但是無法找到足夠大的連續空間來分配當前對象,就會導致不得不 提前觸發一次 FUll GC。

前面介紹到的幾種垃圾回收器,或多或少的都是有延續之前的思想,複製,標記清除,整理,或是eden /to survivor/ from survivor 等,但是對於 G1 垃圾收集器來說 。對之前的禁錮都有了一定的推翻,使用了全新的模式,也是在未來的時間裏,將要推到最前端的垃圾回收器。

G1 垃圾回收器

在這裏插入圖片描述
運行方式:
在這裏插入圖片描述
出現的初衷: 在未來可以替換掉CMS收集器
在進行G1的講解之前,首先來了解G1與其相關的概念:

基礎概念:

吞吐量:在一個指定的時間內,最大化一個應用的工作量
例如一個使用下面的方式來衡量一個系統吞吐量的好壞:

  • 在一個小時內同一個事務(或者說是任務)完成的次數(tps)
  • 數據庫一個小時可以完成多少次查詢。
    對於關注吞吐量的系統,對於我們來說卡頓在日常是可以接受的,因爲對於我們來說,關注一個系統關注的是長時間的大量任務的執行能力,單詞的響應並不值得我們去追求和考慮、

響應能力: 一個程序或者是系統能否及時響應,多久完成響應。比如:在解決到一個請求時候需要多久能夠完成這個請求。

簡介:設計目標

是一個面向服務端的垃圾收集器,適用於多核處理器,大內存容量的服務端系統。能夠滿足短時間GC停頓的同時達到一個較高的吞吐量。

  • 與應用線程同時工作,幾乎不需要stop the world (與 CMS類似)
  • 整理剩餘空間時候,不會產生內存碎片(前面我們講到了是對於 CMS來說 只能再 Full GC 時候,用 stop the world)但是對於 G1來說,一是沒有內存碎片,但是也不用等到 Full gc。對於CMS的full gs 時候,無論是新生代,還是老年代,都是會進行全部的垃圾回收,此時就需要線程進行等待,這個也是會搶佔cpu的地方。(在所有的垃圾收集器的設計中,都會避免出現 full GC 的情況。)
  • 停頓時間更加可控 說的是對於 g1來說,我們可以在啓動的時候 設置一個停頓的時間,例如和cms進行比較的時候,cms在進行full gc 時停頓的時間是不能夠控制的,有時候就算是停頓很長的時間,也是沒有辦法的,但是對於g1 來說 我們設置一個時間,就算是有很多的垃圾需要回收時候,g1也會先進性一個評估,評估大概需要多久,最後 回收的也只是在對應時間差不多的空間大小,等到下一次的再度回收,可以控制。與堆的設計相關,看來也是比較重要的一部分知識啊。
  • 不犧牲系統吞吐量: 指的是說 前面我們也有講到了 cms的不好的地方,就還是說 在併發執行階段,雖然說 不會使用戶線程停止但是也是會佔用一部分的線程,會使得應用程序變慢。
  • gc 不要求額外的內存空間 (CMS需要預留空間 存儲 浮動垃圾)這裏說明什麼是浮動垃圾,就是說 對於cms來說 ,在執行的時候,用戶的線程還是在執行的,開始任務不是垃圾,不會進行回收,但是後來變成了垃圾,需要回收時候,此時的cms沒有能夠認爲是垃圾。也是比較好的地方對於 cms來說

G1與CMS的優勢。

  • 在壓縮空間有優勢:
  • 內存分區region 不再固定。
  • 各個代不需要指定大小
  • 控制時間 控制垃圾收集時間,避免雪崩
  • 在回收內存以後,會立馬進行合併內存的操作,但是cms要進行stop the word
  • G1 可以在yong 但是cms只能老年代。
  • 一個是複製,一個是 標記整理,不會有內碎片產生。
  • 同比較 parallel Scavenge和 parallel Old 比較時候 parallel 會對整個區域做整理,此時的時間停頓比較長
  • 前面講到了會根據用戶設定的停頓時間,會智能評估,回收哪幾個的時候可以滿足用戶的設定

收集集合(Cset)

是一組可被回收的分區的集合,在CSet中存活的數據會在GC的過程中被移動到另一個可用分區,CSet中的分區可以來自eden空間,survivor空間,或者老年代:說是一種準備要被回收的數據的集合。

已記憶集合(RSet)

記錄了其他的Region對象引用本Region中對象的關係,屬於points-inot (誰引用了我的對象)。其價值在於,使得垃圾收集器不需要掃描整個堆找到誰引用了當前分區中的對象,只需要掃描RSet即可,Region就是前面圖片中的一個個小的方格,其中存放的都是對象。
在這裏插入圖片描述
如圖所示,1引用了2中的對象,3也是如此,此時2就會有一個內存空間用於記錄誰引用了我
在這裏插入圖片描述
在這裏插入圖片描述
年輕代:
在這裏插入圖片描述

堆內存劃分。

注:這裏爲什麼要講解堆的劃分,也是讓我們在後面的理解中更加深刻的理解G1垃圾收集器的內存結構,同時也是與CMS進行一個比較(因爲G1設計的初衷就是作爲併發標記收集器(CMS)的長期替代產品)
對於cms來說本質上是使用 mark-sweep 算法,但是對於 G1是從整體上看是基於“標記-整理”算法實現的,但是對於局部的(region)上來看是基於“複製”算法。但是無論如何都意爲這G1在長期的運行期間都不會產生內存空間碎片,收集後也能夠提供規整的空間。自然就可以高效整理剩餘的內存,也就不需要管理內存碎片。。也不需要因爲長期的運行時候會發生分配較大的對象而找不到連續的內存空間而觸發 Full GC。

基礎的模式:

在這裏插入圖片描述
傳統的 新創建的對象位於eden 中 首先是在其中進行創建,然後進行回收時候,將 eden 和s0全部清空 放到 s1裏面去 下一次 進行進行eden創建 將 s1 和eden 清空 放到 s0去 反覆。

G1垃圾收集器的結構

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

併發執行時候的基礎流程
  • 當執行垃圾收集時,G1以類似於CMS收集器的方式運行。
    G1執行併發全局標記階段,以確定整個堆中對象的活動性。標記階段完成後,G1知道哪些區域大部分爲空。它首先收集在這些區域中,通常會產生大量的自由空間。這就是爲什麼這種垃圾收集方法稱爲“垃圾優先”的原因。顧名思義,G1將其收集和壓縮活動集中在可能充滿可回收對象(即垃圾)的堆區域。G1使用暫停預測模型來滿足用戶定義的暫停時間目標,並根據指定的暫停時間目標選擇要收集的區域數。
  • 由G1標識爲可回收的成熟區域是使用疏散收集的垃圾。G1將對象從堆的一個或多個區域複製到堆上的單個區域,並在此過程中壓縮並釋放內存。撤離是在多處理器上並行執行的,以減少暫停時間並增加吞吐量。因此,對於每個垃圾收集,G1都在用戶定義的暫停時間內連續工作以減少碎片。這超出了前面兩種方法的能力。CMS(併發標記掃描)垃圾收集器不進行壓縮。ParallelOld垃圾回收僅執行整個堆壓縮,這導致相當長的暫停時間。

運行的主要模式

young

young 在eden 充滿時候觸發,在回收之後 就變成了空白的
在這裏插入圖片描述
主要流程:
在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

如下主要步驟:

如我們講到的,先進行一個標記的過程,然後將有生命的問題撤離(即複製移動)到一個或多個倖存區域,若是滿足老化闕值,直接被升到老年代。
在這裏插入圖片描述

有的已經被疏散到倖存者地區或是老年代。
在這裏插入圖片描述

老年代

上面介紹到了G1在併發階段,在新生代的情況,但是G1還有一個就是回合mixed,可以對新生代操作的同時也會對老年代進行操作:
在這裏插入圖片描述
在這裏插入圖片描述
這裏進行一個回顧,前面我們有講到的是對於G1來說會有兩種機制一種是 young一種是mixed,其中 young只是會對young進行,但是 mixed就會是young和 老年代同時進行 ,此時對於mixed來說 其中有一個併發標記,作用是在老年代裏面查詢得出收益高的若干老年代,其中的併發標識使用到的就是 GCM。當mixed也不能夠滿足要求的時候 ,就會退化爲full GC,導致stop the word

具體過程

階段 描述
Initial Mark 初始化標記時候會導致stop the word ,在G1中,會在young GC 上進行,在標記倖存者區域(根區域的時候)可能會有對老年代對象的引用。
根區域的掃描 掃描倖存者區域以獲取老年代的參考,在進行新生代的GC回收之前必須完成此階段。
併發標記 在整個堆中查找活動的對象。併發過程運行時候會發生這樣的情況,但是這個階段可以被年輕代垃圾收集器中斷
重新標記(STW) 完成的是堆中活動對象的標記。使用一種稱爲快照(SATB)算法,此算法交CMS收集器中使用到的要快的多,在後面會進行介紹
清理(STW) 1. 對活動對象和完全空閒區域進行記錄 2. 進行RSet(會發生STW)
複製 在stop the word 之後,將之前進行標記過的賦值到全新的未使用的地區中

下面介紹一下STAB算法

STAB

在這裏插入圖片描述在併發階段可能會遇到的兩個問題
二是創建新的對象:
在這裏插入圖片描述
一個是 對象引用的變更:
在這裏插入圖片描述
併發標記是併發多線程的,但併發線程在同一個時刻也是隻會掃描同一個分區


前面介紹到了使用 gcm 進行統計收益比較高的老年代,下面介紹GCM。

gcm

前面說到的是 G1 可以適用於 老年代和新生代,對於 mixed 就是老年代的 。但是怎麼說呢 gcm的執行過程類始於cms 但是不同的是,他主要是爲mix 提供一個標記服務的,並不是G1 的一定要有的一部分 。因爲對於G1來說有時候沒有Mixed GC 時候 ,就不會進行所謂的GCM

GCM四個步驟:

  • 初始標記: 會出現stop 會從Gc Root開始直接可達的對象
  • 併發標記: 從GC Root 開始對 heap 中的對象進行標記,這個標記的過程和應用程序線程併發執行,並且收集 位於 region中的對象的存活的情況。
  • 重新標記: 標記那些在併發階段發生變化的對象,將被回收。
  • 清理 清空region,那些region中已經沒有了存活的對象 加入 free list。
    在這裏插入圖片描述

gcm 結束之後

我們可以設計一些的參數進行基礎控制
在這裏插入圖片描述
上面所說的進行GCM時候需要判斷垃圾的佔比就需要用到此參數。看到的是存活對象的佔比在某一個值的下面,也就不是意味着垃圾佔比在某一個值的上面纔會入選CSet
在這裏插入圖片描述

G1 不提供 full gc 但是若是mixgc 無法根上內存的分配的速度 就會觸發 serial old GC 收集整個GC 。

Humongous 區域

當然會與一些特殊的區域G1也會有對應的處理方法
在這裏插入圖片描述

G1的最佳實踐

  • 不斷調優暫停時間指標
    通過 -XX:MaxGCPauseMills=x 可以設置啓動引用程序暫停的時間,G1在運行時候 會根據這個參數選擇CSet來滿足響應時間的設置。一般都設置在 100到 200 ms
    面試問 爲什麼設置這麼久: 大概是因爲若是設計的時間太短的話,會導致出現 G1跟不上垃圾產生的速度,就會導致最終的full gc 出現。
  • 不要設置 新生代 和老年代的大小
    不要設置這些 我們可以設置 整個堆內存大小 不設置 也會有自己的啓動至
    在這裏插入圖片描述

後記

以上就是我在看《深入理解java虛擬機》和一些相關聯的視頻整理出來的自我認知有什麼不是很合適的地方,還請大家指出來,一切學習進步。

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