深入探究JVM之垃圾回收器

来源:https://www.cnblogs.com/yewy/archive/2020/07/25/13377709.html
-Advertisement-
Play Games

@ 前言 JVM的自動記憶體管理得益於不斷發展的垃圾回收器,從最初的單線程收集到現在併發收集,垃圾回收器的開發者們一直在致力於如何降低GC過程中的停頓時間(STW)以及提高吞吐量,但直到現在也不存在一款完美的垃圾回收器,只能根據不同的場景選擇最合適的。所以需要瞭解每款垃圾回收器出現的背景、原因,並掌握 ...


@目錄

前言

JVM的自動記憶體管理得益於不斷發展的垃圾回收器,從最初的單線程收集到現在併發收集,垃圾回收器的開發者們一直在致力於如何降低GC過程中的停頓時間(STW)以及提高吞吐量,但直到現在也不存在一款完美的垃圾回收器,只能根據不同的場景選擇最合適的。所以需要瞭解每款垃圾回收器出現的背景、原因,並掌握各種垃圾回收器的設計原理、演算法實現細節以及各個垃圾回收器的優劣對比,這樣才能讓我們在調優時做出最合適的選擇。這部分內容博主準備分為兩篇文章進行總結講解,本篇主要是對垃圾收集演算法的思想以及目前穩定商用的垃圾回收器的講解。

正文

一、垃圾收集演算法

上文分析了JVM判斷對象存活的兩種演算法:引用計數可達性分析。因此垃圾收集演算法的實現也對應的分為引用計數式收集追蹤式收集,而目前JVM中都沒有使用引用計數演算法,所以後面講解的演算法都屬於追蹤式收集。其細分又分為標記-複製標記-清除標記-整理分代回收

標記-複製

複製演算法最初的理論是將可用記憶體分為1:1的兩塊,每次只使用其中一塊,當這塊記憶體滿後,就先標記存活對象並將其複製到另一塊記憶體,然後將滿的記憶體釋放掉。這種演算法非常簡單高效,只需要將標記的存活對象複製到另一半空間,同時記憶體始終保持規整,不會出現記憶體碎片,但缺點也很明顯,可用記憶體減少了一半,另外複製的對象不能太大,否則複製的效率會比較低。
因為新生代中的對象大多“朝生夕死”,在JVM新生代中的垃圾收集器都是採用的複製演算法。但是為避免浪費的空間太多,提出了一種更為優化的複製演算法,稱為Appel式回收。該演算法不再是簡單的“半區複製”,而是將新生代分為了三塊:一塊Eden區和兩塊Survivor區(分別標記為from和to),預設的分配比例是8:1:1(-XX:SurvivorRatio=8表示兩個Survivor區和Eden區比例為2:8,即每個Survivor占10%),每次分配對象都只使用Eden區和其中一塊Survivor區(from區)。其中Eden區最大,新對象都在該區域創建,當Eden區滿後,會進行一次MinorGC,並將Eden區和from區中存活對象都複製到to區中,然後調換from和to指針。當然肯定是存在to區裝不下一次MinorGC存活對象的情況,這時就需要老年代進行分配擔保(相關概念在上一篇已經講過)。
從上面的演算法過程中堵著門應該會有一個疑惑:為什麼需要兩個Survivor區?這裡以假設法進行分析。如果沒有Survivor區,那麼新生代每次GC後存活對象會直接進入老年代,導致老年代迅速填滿,頻繁的觸發FullGC;如果只有一塊Survivor區,那麼為了保證複製演算法的特性(記憶體規整和高效),Eden區經過一次MinorGC後會將對象複製到Survivor區,這時新對象只能在Survivor區創建,否則無法保證記憶體規整,但又由於Survivor區非常小,就會導致很快又觸發有一次MinorGC;而如果有兩塊Survivor區就很好的解決了上面所說的問題,而更多的Survivor區就沒有必要了。

標記-清除

標記清除是最早出現的垃圾回收演算法,由Lisp之父提出。這個演算法也很簡單,首先標記存活的對象,然後統一回收未被標記的對象。相較於複製演算法的缺點也很明顯,效率更低,同時會導致記憶體碎片。為什麼效率更低了呢,好比你刪除文件,直接格式化文件夾快還是去文件夾中找到文件一個個刪除更快?另外記憶體碎片會導致堆中明明還有足夠的記憶體,但卻沒有足夠的連續記憶體來存放大對象,導致對象直接進入老年代。

標記-整理

這個演算法就是建立在標記清除的基礎之上,多了一步整理的工作,標記完成後首先將存活的對象移動到一邊,然後清理掉另一邊的記憶體,解決了記憶體碎片帶來的問題。標記-清除標記-整理都適合用在老年代中,而前者相較於後者不用移動記憶體,而移動記憶體是一種非常“危險”的操作,需要暫停其它用戶線程的執行,確保記憶體指向的正確性,所以這就是STW出現的原因,就好比你不能在你媽媽打掃屋子的同時邊往地上扔垃圾。

分代回收

分代回收嚴格意義上並不算一種演算法,而是各回收演算法的實踐理論。它建立在兩個分代假說之上:

  • 弱分代假說(Weak Generational Hypothesis):絕大多數對象都是朝生夕滅的。
  • 強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消
    亡。

上面兩個假說共同確定了垃圾收集器一致的設計原則,即新生代老年代。在新生代中使用複製演算法,如上所說,大部分對象朝生夕滅,所以只需要將少量存活對象複製到另一塊區域後再統一格式化之前的區域;而老年代因為大量對象存活,只能採用標記清除標記整理演算法。

二、常用的垃圾回收器

垃圾回收器是垃圾回收演算法的實現,在虛擬機規範中並沒有定義要如何實現垃圾回收器,所以各大廠商對垃圾回收器的實現有很大差別,但都是在朝著一個方向努力:低延遲、高吞吐量。
在這裡插入圖片描述

上圖中展示的就是目前主流的垃圾回收器,有連線的代表兩者可以搭配使用,而打“X”的表示在JDK9中已經廢棄的組合,另外從圖中我們還可以發現除了G1,其它垃圾回收器都只能作用於新生代老年代中的其中一個區域,那麼G1是不是表示廢除了分代理論呢?下麵來逐個介紹。

Serial/SerialOld

這兩個是最早出現的垃圾回收器,如其名,它們都是單線程的垃圾回收器,只適合幾十兆到一兩百兆的堆空間的垃圾回收,如果用於更大的堆空間會導致系統停頓時間較長,想象一下系統每隔一段時間就要停止處理請求幾分鐘甚至更長時間,你能接受麽?下圖是他們的工作原理:
在這裡插入圖片描述
可以看到新生代或老年代在進行垃圾回收時都會暫停所有的用戶線程,圖中的SafePoint表示線程能夠安全暫停的時機,即JVM要進行垃圾回收時,不可能隨意暫停所有的線程,必須要確保線程處於安全點才能暫停它。這裡先有這個概念,細節在下一篇進行闡述。
該組合可以通過-XX:+UseSerialGC參數開啟。

ParNew

該收集器就是Serial的多線程版本,但在單核處理器環境中表現還不如Serial(涉及線程的切換)。它預設開啟的收集線程數與處理器核心數量相同,在處理器核心非常多的環境中,可以使用-XX:ParallelGCThreads參數來限制垃圾收集的線程數。
在這裡插入圖片描述
另外需要註意的是它是除了Serial之外唯一可以與CMS配合的垃圾收集器,在激活CMS後(使用-XX:+UseConcMarkSweepGC選項)的預設新生代收集器,也可以使用-XX:+/-UseParNewGC選項來強制指定或者禁用它,在JDK9以後ParNew成為了CMS的一部分。

Parallel Scavenge/ParallelOld

Parallel Scavenge與其它垃圾收集器不同,其它的是追求儘可能小的GC停頓時間,而它主要關註吞吐量,所謂吞吐量就是代碼運行時間/(代碼運行時間 + 垃圾回收時間)。比如虛擬機運行100分鐘,垃圾回收耗時1分鐘,那麼吞吐量就是99%。但是這款收集器在JDK1.6之前比較尷尬,沒有與之對應的並行的老年代收集器,只能採用SerialOld老年代收集器,使得表現比不上PareNew+CMS的組合。直到ParallelOld出現後,Parallel Scavenge才能真正的展現它吞吐量的優勢。
在這裡插入圖片描述
Parallel Scavenge有以下幾個重要的參數:

  • -XX:MaxGCPauseMillis:該參數的值是一個大於0的毫秒數,收集器儘量保證GC停頓時間不超過該值,但是不要天真的認為該值越小越好。該值設置的太小會導致每次GC的回收率降低,垃圾堆積,GC發生的越來越頻繁。比如原先需要100ms收集500M空間,現在設置為50ms,那麼可能就只能回收300M或者更小的垃圾。
  • -XX:GCTimeRatio:控制垃圾回收時間比率。比如允許最大垃圾回收時間占總時間的5%,那麼需要將該值設置為19(公式是1/(1 + 19))。
  • -XX:+UseAdaptiveSizePolicy:這個參數激活後,就不再需要我們手動設定新生代各區(Eden、from、to)的比例(-XX:SurvivorRatio),晉升老年代對象的大小(-XX:PretenureSizeThreshold),虛擬機會監控運行時的狀態,進行動態的調整,這種方式稱為垃圾收集的自適應調節策略(GC Ergonomics)。

CMS

CMS(Concurrent Mark Sweep)是第一款併發垃圾收集器,併發是指垃圾收集可以和用戶線程同時進行。同時它也是唯一採用標記清除演算法對老年代進行回收的垃圾回收器。它包含了以下幾個階段:

  • 初始標記:STW,只標記與GC Roots直接關聯的對象
  • 併發標記:和用戶線程同時運行,進行可達性分析
  • 重新標記:STW,暫停用戶線程,修正上一階段變動的對象
  • 併發清除:最後是併發的清除掉垃圾

在這裡插入圖片描述
從上面我們可以發現CMS的整個過程中只有初始標記重新標記是需要暫停用戶線程的,而初始標記只是標記與GC Roots直接關聯的對象,所以耗時只和GC Roots的數量有關,非常快;重新標記的耗時會比初始標記略長,但也遠遠比併發標記用時短,所以CMS就是通過細分GC的階段來降低GC的停頓時間。
你可能會好奇為什麼需要重新標記並且暫停所有用戶線程,因為在與用戶線程併發執行的同時肯定會存在引用變動的情況,而要處理這個問題,都是必須要暫停用戶線程的,關於引用變動的處理在下一篇會詳細分析。
CMS可以說是一款跨時代的垃圾收集器,可以回收幾個G到-20G左右的堆空間,但它存在以下幾個明顯的缺點:

  • CPU敏感:雖然併發標記併發標記是和用戶線程併發執行的,但是也因此占用了系統的資源,導致應用程式忽然變慢,降低吞吐量。CMS預設啟動的線程數是(處理器核心數+3)/4,因此當核心數量大於等於4時,GC占用資源不超過25%,但核心數小於4時,就會占用大量系統資源。
  • 大量的記憶體碎片:因為CMS是使用標記清除演算法實現垃圾回收,所以會產生大量的記憶體碎片。為了避免這個問題,CMS採用了一個折中的辦法,即提供一個-XX:+UseCMS-CompactAtFullCollection參數,該參數預設開啟,控制CMS在進行FullGC的同時進行空間整理,但這樣又會導致停頓時間加長,所以還提供了-XX:CMSFullGCsBefore-Compaction參數,控制CMS在進行了多少次不帶整理的FullGC後進行一次帶整理的FullGC,預設值是0,即每次FullGC都會整理,該參數JDK9後被廢棄。
  • 浮動垃圾:因為最終清除的過程也是和用戶線程併發執行的,因此這個過程中必然會產生新的垃圾,這一部分垃圾需要預留空間來存放,等待下一次GC的時候再清理,因此會浪費一部分空間。在JDK5的預設配置下,當老年代使用空間超過68%時就會進行GC,到JDK6時,這個閾值就提高到了92%,另外也可以通過-XX:CMSInitiatingOccu-pancyFraction參數控制。但該值越高,那麼併發清理過程中可使用的記憶體就越小,當放不下時,就會出現一次Concurrent Mode Failure,這時候虛擬機就會凍結線程並採用SerialOld進行垃圾回收,導致停頓時間變得更長。

Garbage First

G1是目前最前沿且可商用的垃圾收集器,另外還有ZGC等更為前沿的垃圾收集器還處於試驗階段。它與其它垃圾收集器不同的是,他將堆空間化整為零,將記憶體區域劃分為多個大小相等的獨立區域(Region),使得它可以回收堆中的任何一個區域,而不是像其它的垃圾收集器要麼只能回收新生代,要麼只能回收老年代。但不是說G1就沒有新生代和老年代了,它的每個Region都可以根據需要扮演Eden、Survivor或老年代,垃圾收集器也會針對不同角色的Region採用不同的策略去處理。
在這裡插入圖片描述
每個Region的大小可以通過-XX:G1HeapRegionSize設定,取值範圍為1M~32M,且必須為2的N次冪。超過單個Region一半容量的對象即為大對象,而對於超過整個Region的對象將會使用多個連續的Humongous空間存放,G1大多數情況下都把Humongous作為老年代一部分看待。
在這裡插入圖片描述
G1的運行過程如上,它也包含了以下4個步驟:

  • 初始標記:STW,也是只標記GC Roots直接關聯的對象,並修改TAMS的指針值(G1為每一個Region設計了兩個名為TAMS(Top at Mark Start)的指針,把Region中的一部分空間劃分出來用於併發回收過程中的新對象分配,併發回收時新分配的對象地址都必須要在這兩個指針位置以上,垃圾回收時也不會回收這部分空間),這個過程耗時很短,而且是借用進行 Minor GC 的時候同步完成的,所以 G1 收集器在這個階段實際並沒有額外的停頓。
  • 併發標記:可達性分析找出要回收的對象,在對象掃描完成後,由於是與用戶線程併發執行的,所以存在引用變動的對象,這部分對象會由SATB演算法來解決(原始快照,下一篇詳細分析)。
  • 最終標記:STW,處理併發階段遺留的少量遺留的SATB記錄。
  • 篩選回收:根據用戶設定的-XX:MaxGCPauseMillis最大GC停頓時間對Region進行排序,並回收價值最大的Region,儘量保證滿足參數設定的值(該值效果和Parallel Scavenge部分講解的是一樣的)。這裡的回收演算法就是講存活的對象複製到空的Region中,即G1局部Region之間採用的是複製演算法,而整體上採用的是標記整理演算法

G1適合上百G的堆空間回收,與CMS的權衡在6~8G之間,較大的堆記憶體才能凸顯G1的優勢,可以通過-XX:+UseG1GC參數開啟。

總結

本篇是對常用垃圾收集器的實現原理的整體性分析比較,這一部分是必須掌握的,下一篇則是關於演算法的實現細節,如三色標記是什麼、併發標記過程中引用變動如何解決、跨代引用如何處理等等一系列問題。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 數據註釋是能夠運用於類或類成員的特點,以指定類之間的聯繫、描述數據怎麼在UI中顯現以及指定驗證規矩。本文評論數據註釋、為什麼數據註釋很有用以及怎麼在.NETCore應用程式中運用它們。 若要運用本文供給的代碼示例,您應該在體系中裝置VisualStudio2019。如果還沒有裝置,能夠在此處下載Vi ...
  • C#(讀作“SeeSharp”)是一種新式編程言語,不僅面向目標,還類型安全。C#源於C言語系列,C、C++、Java和JavaScript程式員很快就可以上手使用。 本教程概述了C#8及更高版別中該言語的首要組件。假如想要經過互動式示例探索言語,請嘗試C#簡介教程。 C#是一種面向目標的言語。不僅 ...
  • Java語言是一種面向的程式設計語言,而面向對象思想是一種程式設計思想。我們參照面向對象思想使用java語言去設計,開發電腦程式。 除去面向對象之外還有一個面向過程。 區別: 面向過程:當要實現一個功能的時候,面向過程,要處理好每一個節點,直至整個過程結束,然後得到結果。 面向對象:當要實現一個功 ...
  • 1. 前言 歡迎閱讀Spring Security 實戰乾貨系列文章,在集成Spring Security安全框架的時候我們最先處理的可能就是根據我們項目的實際需要來定製註冊登錄了,尤其是Http登錄認證。根據以前的相關文章介紹,Http登錄認證由過濾器UsernamePasswordAuthent ...
  • 3.色彩空間 色彩空間¶ 下麵的圖的三個點表示的是RGB,當三個通道全是0時是黑色,全是255時是白色。 常見的色彩空間 1.色彩空間轉換的API¶ cv.cvtColor(圖片,cv.COLOR_BGR2+“色彩空間”) In [4]: import cv2 as cv def color_spa ...
  • 2.圖像方面Numpy數組相關操作 In [1]: import cv2 as cv import numpy as np #圖片顏色反轉 def access_pixels(img): print(img.shape) height=img.shape[0] width=img.shape[1] ...
  • 1.opencv基礎 In [1]: import cv2 as cv #讀出video #打開指定路徑下的視頻文件:cap =cv2.VideoCapture(path) #讀取每一幀:flag,frame = cap.read(),打開視頻並讀取每一幀圖片,將視頻轉換為4維的矩陣 def vid ...
  • 點擊此處進入下載地址 提取碼:2wg3 資料簡介: 本書採用獨創的黑箱模式,MBA案例教學機制,結合一線實戰案例,介紹Sklearn人工智慧模塊庫和常用的機器學習演算法。書中配備大量圖表說明,沒有枯燥的數學公式,普通讀者,只要懂Word、Excel,就能夠輕鬆閱讀全書,並學習使用書中的知識,分析大數據 ...
一周排行
    -Advertisement-
    Play Games
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...