图文并茂,万字详解,带你掌握 JVM 垃圾回收!
垃圾回收( Garbage Collection 以下簡稱 GC)誕生于1960年 MIT 的 Lisp 語言,有半個多世紀的歷史。
在Java 中,JVM 會對內存進行自動分配與回收,其中 GC 的主要作用就是清楚不再使用的對象,自動釋放內存。
GC 相關的研究者們主要是思考這3件事情。
-
哪些內存需要回收?
-
什么時候回收?
-
如何回收?
本文也大致按照這個思路,為大家描述垃圾回收的相關知識。因為會有很多內存區域相關的知識,希望讀者先學習完《精美圖文帶你掌握 JVM 內存布局》再來閱讀本文。
本文的主要內容如下(建議大家在閱讀和學習的時候,也大致按照以下的思路來思考和學習):
-
哪些內存需要回收?即GC 發生的內存區域?
-
如何判斷這個對象需要回收?即GC 的存活標準?
這里又能夠引出以下的知識概念:
-
引用計數法
-
可達性分析法
-
引用的種類和特點、區別?(強引用、軟引用、弱引用、虛引用)
-
延伸知識:(WeakHashMap) (引用隊列)
有了對象的存活標準之后,我們就需要知道GC 的相關算法(思想)
-
標記-清除(Mark-Sweep)算法
-
復制(Copying)算法
-
標記-整理(Mark-Compact)算法
在下一步學習之前,還需要知道一些GC的術語,防止對一些概念描述出現混淆,知道了算法之后,自然而然我們到了JVM中對這些算法的實現和應用,即各種垃圾收集器(Garbage Collector)
-
串行收集器
-
并行收集器
-
CMS 收集器
-
G1 收集器
一、GC 的 目標區域
一句話:GC 主要關注 堆和方法區
在精美圖文帶你掌握 JVM 內存布局一文中,理解介紹了Java 運行時內存的分布區域和特點。
其中我們知道了程序計數器、虛擬機棧、本地方法棧3個區域是隨線程而生,隨線程而滅的。棧中的棧幀隨著方法的進入和退出而有條不紊地執行著出棧和入棧操作。
每一個棧幀中分配多少內存基本上是在類結構確定下來時就已知的(盡管在運行期會由JIT編譯器進行一些優化,但在本章基于概念模型的討論中,大體上可以認為是編譯期可知的),因此這幾個區域的內存分配和回收都具備確定性,在這幾個區域內就不需要過多考慮回收的問題,因為方法結束或者線程結束時,內存自然就跟隨著回收了。
而堆和方法區則不一樣,一個接口中的多個實現類需要的內存可能不一樣,一個方法中的多個分支需要的內存也可能不一樣,我們只有在程序處于運行期間時才能知道會創建哪些對象,這部分內存的分配和回收都是動態的。GC 關注的也就是這部分的內存區域。
二、GC 的存活標準
知道哪些區域的內存需要被回收之后,我們自然而然地想到了,如何去判斷一個對象需要被回收呢?
對于如何判斷對象是否可以回收,有兩種比較經典的判斷策略。
-
引用計數算法
-
可達性分析算法
1. 引用計數法
在對象頭維護著一個 counter 計數器,對象被引用一次則計數器 +1;若引用失效則計數器 -1。當計數器為 0 時,就認為該對象無效了。
主流的Java虛擬機里面沒有選用引用計數算法來管理內存,其中最主要的原因是它很難解決對象之間相互循環引用的問題。發生循環引用的對象的引用計數永遠不會為0,結果這些對象就永遠不會被釋放。
2. 可達性分析算法?
從GC Roots 為起點開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到GC Roots 沒有任何引用鏈相連時,則證明此對象是不可用的。不可達對象。
Java 中,GC Roots 是指:
-
Java 虛擬機棧(棧幀中的本地變量表)中引用的對象
-
本地方法棧中引用的對象
-
方法區中常量引用的對象
-
方法區中類靜態屬性引用的對象
3. Java 中的引用?
Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)、虛引用(Phantom Reference)4種,這4種引用強度依次逐漸減弱。
這樣子設計的原因主要是為了描述這樣一類對象:當內存空間還足夠時,則能保留在內存之中;如果內存空間在進行垃圾收集后還是非常緊張,則可以拋棄這些對象。很多系統的緩存功能都符合這樣的應用場景。
也就是說,對不同的引用類型,JVM 在進行GC 時會有著不同的執行策略。所以我們也需要去了解一下。
A. 強引用(Strong Reference)
MyClass obj = new MyClass(); // 強引用 obj = null // 此時‘obj’引用被設為null了,前面創建的'MyClass'對象就可以被回收了只要強引用存在,垃圾收集器永遠不會回收被引用的對象,只有當引用被設為null的時候,對象才會被回收。但是,如果我們錯誤地保持了強引用,比如:賦值給了 static 變量,那么對象在很長一段時間內不會被回收,會產生內存泄漏。
B. 軟引用(Soft Reference)
軟引用是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當 JVM 認為內存不足時,才會去試圖回收軟引用指向的對象。JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用通常用來實現內存敏感的緩存,如果還有空閑內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。
SoftReference<MyClass> softReference = new SoftReference<>(new MyClass());C. 弱引用(Weak Reference)
弱引用的強度比軟引用更弱一些。當 JVM 進行垃圾回收時,無論內存是否充足,都會回收只被弱引用關聯的對象。
WeakReference<MyClass> weakReference = new WeakReference<>(new MyClass());弱引用可以引申出來一個知識點, WeakHashMap&ReferenceQueue
ReferenceQueue 是GC回調的知識點。這里因為篇幅原因就不細講了,推薦引申閱讀:ReferenceQueue的使用
D. 幻象引用/虛引用(Phantom References)
虛引用也稱為幽靈引用或者幻影引用,它是最弱的一種引用關系。一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
PhantomReference<MyClass> phantomReference = new PhantomReference<>(new MyClass(), new ReferenceQueue<>());三、GC 算法?
有了判斷對象是否存活的標準之后,我們再來了解一下GC的相關算法。
-
標記-清除(Mark-Sweep)算法
-
復制(Copying)算法
-
標記-整理(Mark-Compact)算法
1. 標記-清除(Mark-Sweep)算法
標記-清除算法在概念上是最簡單最基礎的垃圾處理算法。
該方法簡單快速,但是缺點也很明顯,一個是效率問題,標記和清除兩個過程的效率都不高;另一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后在程序運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
后續的收集算法都是基于這種思路并對其不足進行改進而得到的。
2. 復制(Copying)算法
復制算法改進了標記-清除算法的效率問題。
它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。
這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。
缺點也是明顯的,可用內存縮小到了原先的一半。
現在的商業虛擬機都采用這種收集算法來回收新生代,IBM公司的專門研究表明,新生代中的對象98%是“朝生夕死”的,所以并不需要按照1:1的比例來劃分內存空間,而是將內存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。
在前面的文章中我們提到過,HotSpot默認的Eden:survivor1:survivor2=8:1:1,如下圖所示。
延伸知識點:內存分配擔保
當然,98%的對象可回收只是一般場景下的數據,我們沒有辦法保證每次回收都只有不多于10%的對象存活,當Survivor空間不夠用時,需要依賴其他內存(這里指老年代)進行分配擔保(Handle Promotion)。
內存的分配擔保就好比我們去銀行借款,如果我們信譽很好,在98%的情況下都能按時償還,于是銀行可能會默認我們下一次也能按時按量地償還貸款,只需要有一個擔保人能保證如果我不能還款時,可以從他的賬戶扣錢,那銀行就認為沒有風險了。內存的分配擔保也一樣,如果另外一塊Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象時,這些對象將直接通過分配擔保機制進入老年代。
3. 標記-整理算法
前面說了復制算法主要用于回收新生代的對象,但是這個算法并不適用于老年代。因為老年代的對象存活率都較高(畢竟大多數都是經歷了一次次GC千辛萬苦熬過來的,身子骨很硬朗😎)
根據老年代的特點,提出了另外一種標記-整理(Mark-Compact)算法,標記過程仍然與“標記-清除”算法一樣,但后續步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。
4. 分代收集算法
有沒有注意到了,我們前面的表述當中就引入了新生代、老年代的概念。準確來說,是先有了分代收集算法的這種思想,才會將Java堆分為新生代和老年代。這兩個概念之間存在著一個先后因果關系。
這個算法很簡單,就是根據對象存活周期的不同,將內存分塊。在Java 堆中,內存區域被分為了新生代和老年代,這樣就可以根據各個年代的特點采用最適當的收集算法。
就如我們在介紹上面的算法時描述的,在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用?“標記—清理”?或者?“標記—整理” 算法?來進行回收。
-
新生代:復制算法
-
老年代:標記-清除算法、標記-整理算法
5. 重新回顧 創建對象時觸發GC的流程
這里重新回顧一下精美圖文帶你掌握 JVM 內存布局里面JVM創建一個新對象的內存分配流程圖。這張圖也描述了GC的流程。
四、GC 術語?
在學習垃圾收集器知識點之前,需要向讀者們科普一些GC的術語,方便你們后面理解。
-
部分收集(Partial GC):指目標不是完整收集整個Java堆的垃圾收集,其中又分為:
-
新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
-
老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。另外請注意“Major GC”這個說法現在有點混淆,在不同資料上常有不同所指,讀者需按上下文區分到底是指老年代的收集還是整堆收集。
-
混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。
-
-
整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。
-
并行(Parallel)?:在JVM運行時,同時存在應用程序線程和垃圾收集器線程。并行階段是由多個GC 線程執行,即GC 工作在它們之間分配。
串行(Serial):串行階段僅在單個GC 線程上執行。
STW?:Stop The World 階段,應用程序線程被暫停,以便GC線程 執行其工作。當應用程序因為GC 暫停時,這通常是由于Stop The World 階段。
并發(Concurrent):用戶線程與垃圾收集器線程同時執行,不一定是并行執行,可能是交替執行(競爭)
增量:如果一個階段是增量的,那么它可以運行一段時間之后由于某些條件提前終止,例如需要執行更高優先級的GC 階段,同時仍然完成生產性工作。增量階段與需要完全完成的階段形成鮮明對比。
五、垃圾收集器?
知道了算法之后,自然而然我們到了JVM中對這些算法的實現和應用,即各種垃圾收集器(Garbage Collector)。
首先要認識到的一個重要方面是,對于大多數JVM,需要兩種不同的GC算法,一種用于清理新生代,另一種用于清理老年代。
意思就是說,在JVM中你通常會看到兩種收集器組合使用。下圖是JVM 中所有的收集器(Java 8 ),其中有連線的就是可以組合的。
為了減小復雜性,快速記憶,我這邊直接給出比較常用的幾種組合。其他的要么是已經廢棄了要么就是在現實情況下不實用的。
接下去我們開始具體介紹上各個垃圾收集器。這里需要提一下的是,我這邊是將垃圾收集器分成以下幾類來講述的:
-
Serial GC
-
Parallel GC
-
Concurrent Mark and Sweep (CMS)
-
G1 - Garbage First
理由無他,我覺得這樣更符合理解的思路,你更好理解。
關注微信公眾號:Java技術棧,在后臺回復:JVM,可以獲取我整理的 N 篇最新JVM?教程,都是干貨。
4.1 串行收集器
Serial 翻譯過來可以理解成單線程。單線程收集器有Serial 和 Serial Old 兩種,它們的唯一區別就是:Serial 工作在新生代,使用“復制”算法,Serial Old 工作在老年代,使用“標志-整理”算法。所以這里將它們放在一起講。
串行收集器收集器是最經典、最基礎,也是最好理解的。它們的特點就是單線程運行及獨占式運行,因此會帶來很不好的用戶體驗。雖然它的收集方式對程序的運行并不友好,但由于它的單線程執行特性,應用于單個CPU硬件平臺的性能可以超過其他的并行或并發處理器。
“單線程”的意義并不僅僅是說明它只會使用一個處理器或一條收集線程去完成垃圾收集工作,更重要的是強調在它進行垃圾收集時,必須暫停其他所有工作線程,直到它收集結束(STW階段)。
STW 會帶給用戶惡劣的體驗,所以從JDK 1.3開始,一直到現在最新的JDK 13,HotSpot虛擬機開發團隊為消除或者降低用戶線程因垃圾收集而導致停頓的努力一直持續進行著,從Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)和Garbage First(G1)收集器,最終至現在垃圾收集器的最前沿成果Shenandoah和ZGC等。
雖然新的收集器很多,但是串行收集器仍有其適合的場景。迄今為止,它依然是HotSpot虛擬機運行在客戶端模式下的默認新生代收集器,有著優于其他收集器的地方,那就是簡單而高效。對于內存資源受限的環境,它是所有收集器里額外內存消耗最小的,單線程沒有線程交互開銷。(這里實際上也是一個時間換空間的概念)
通過JVM參數?-XX:+UseSerialGC?可以使用串行垃圾回收器(上面表格也有說明)
4.2 并行收集器
按照程序發展的思路,單線程處理之后,下一步很自然就到了多核處理器時代,程序多線程并行處理的時代。并行收集器是多線程的收集器,在多核CPU下能夠很好的提高收集性能。
這里我們會介紹:
-
ParNew
-
Parallel Scavenge
-
Parallel Old
這里還是提供太長不看版白話總結,方便理解。因為我知道有些人剛開始學習JVM 看這些名詞都會覺得頭暈。
-
ParNew收集器 就是 Serial收集器的多線程版本,基于“復制”算法,其他方面完全一樣,在JDK9之后差不多退出歷史舞臺,只能配合CMS在JVM中發揮作用。
-
Parallel Scavenge 收集器 和 ParNew收集器類似,基于“復制”算法,但前者更關注可控制的吞吐量,并且能夠通過-XX:+UseAdaptiveSizePolicy打開垃圾收集自適應調節策略的開關。
-
Parallel Old 就是 Parallel Scavenge 收集器的老年代版本,基于“標記-整理”算法實現。
A. ParNew 收集器
ParNew收集器除了支持多線程并行收集之外,其他與Serial收集器相比并沒有太多創新之處,但它卻是不少運行在服務端模式下的HotSpot虛擬機,尤其是JDK 7之前的遺留系統中首選的新生代收集器,其中有一個與功能、性能無關但其實很重要的原因是:除了Serial收集器外,目前只有它能與CMS收集器配合工作。
但是從G1 出來之后呢,ParNew的地位就變得微妙起來,自JDK 9開始,ParNew加CMS收集器的組合就不再是官方推薦的服務端模式下的收集器解決方案了。官方希望它能完全被G1所取代,甚至還取消了『ParNew + Serial Old』 以及『Serial + CMS』這兩組收集器組合的支持(其實原本也很少人這樣使用),并直接取消了-XX:+UseParNewGC參數,這意味著ParNew 和CMS 從此只能互相搭配使用,再也沒有其他收集器能夠和它們配合了。可以理解為從此以后,ParNew 合并入CMS,成為它專門處理新生代的組成部分。
B. Parallel Scavenge收集器
Parallel Scavenge收集器與ParNew收集器類似,也是使用復制算法的并行的多線程新生代收集器。但Parallel Scavenge收集器關注可控制的吞吐量(Throughput)
注:吞吐量是指CPU用于運行用戶代碼的時間與CPU總消耗時間的比值,即吞吐量 = 運行用戶代碼時間 /( 運行用戶代碼時間 + 垃圾收集時間 )
Parallel Scavenge收集器提供了幾個參數用于精確控制吞吐量和停頓時間:
C. Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,多線程,基于“標記-整理”算法。這個收集器是在JDK 1.6中才開始提供的。
由于如果新生代選擇了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外別無選擇(Parallel Scavenge無法與CMS收集器配合工作),Parallel Old收集器的出現就是為了解決這個問題。Parallel Scavenge和Parallel Old收集器的組合更適用于注重吞吐量以及CPU資源敏感的場合。
4.3?Concurrent Mark and Sweep (CMS)
CMS(Concurrent Mark Sweep,并發標記清除) 收集器是以獲取最短回收停頓時間為目標的收集器(追求低停頓),它在垃圾收集時使得用戶線程和 GC 線程并發執行,因此在垃圾收集過程中用戶也不會感到明顯的卡頓。
從名字就可以知道,CMS是基于“標記-清除”算法實現的。它的工作過程相對于上面幾種收集器來說,就會復雜一點。整個過程分為以下四步:
1)初始標記 (CMS initial mark):主要是標記 GC Root 開始的下級(注:僅下一級)對象,這個過程會?STW,但是跟 GC Root 直接關聯的下級對象不會很多,因此這個過程其實很快。
2)并發標記 (CMS concurrent mark):根據上一步的結果,繼續向下標識所有關聯的對象,直到這條鏈上的最盡頭。這個過程是多線程的,雖然耗時理論上會比較長,但是其它工作線程并不會阻塞,沒有 STW。
3)重新標記(CMS remark):顧名思義,就是要再標記一次。為啥還要再標記一次?因為第 2 步并沒有阻塞其它工作線程,其它線程在標識過程中,很有可能會產生新的垃圾。
這里舉一個很形象的例子:
就比如你和你的小伙伴(多個GC線程)給一條長走廊打算衛生,從一頭打掃到另一頭。當你們打掃到走廊另一頭的時候,可能有同學(用戶線程)丟了新的垃圾。所以,為了打掃干凈走廊,需要你示意所有的同學(用戶線程)別再丟了(進入STW階段),然后你和小伙伴迅速把剛剛的新垃圾收走。當然,因為剛才已經收過一遍垃圾,所以這次收集新產生的垃圾,用不了多長時間(即:STW 時間不會很長)。
4)并發清除(CMS concurrent sweep):
??? 提問環節:為什么CMS要使用“標記-清除”算法呢?剛才我們不是提到過“標記-清除”算法,會留下很多內存碎片嗎?
確實,但是也沒辦法,如果換成“標記 - 整理”算法,把垃圾清理后,剩下的對象也順便整理,會導致這些對象的內存地址發生變化,別忘了,此時其它線程還在工作,如果引用的對象地址變了,就天下大亂了。
對于上述的問題JVM提供了兩個參數:
另外,由于最后一步并發清除時,并不阻塞其它線程,所以還有一個副作用,在清理的過程中,仍然可能會有新垃圾對象產生,只能等到下一輪 GC,才會被清理掉。
4.4?G1 - Garbage First
JDK 9發布之日,G1宣告取代Parallel Scavenge加Parallel Old組合,成為服務端模式下的默認垃圾收集器。
鑒于 CMS 的一些不足之外,比如: 老年代內存碎片化,STW 時間雖然已經改善了很多,但是仍然有提升空間。G1 就橫空出世了,它對于堆區的內存劃思路很新穎,有點算法中分治法“分而治之”的味道。具體什么意思呢,讓我們繼續看下去。
G1 將連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要,扮演新生代的Eden空間、Survivor空間,或者老年代空間。每個Region的大小可以通過參數-XX:G1HeapRegionSize設定,取值范圍為1MB~32MB,且應為2的N次冪。
Region中還有一類特殊的Humongous區域,專門用來存儲大對象。G1認為只要大小超過了一個Region容量一半的對象即可判定為大對象。對于那些超過了整個Region容量的超級大對象,將會被存放在N個連續的Humongous Region之中。
Humongous,簡稱 H 區,是專用于存放超大對象的區域,通常?>= 1/2 Region Size,G1的大多數行為都把Humongous Region作為老年代的一部分來進行看待。
認識了G1中的內存規劃之后,我們就可以理解為什么它叫做"Garbage First"。所有的垃圾回收,都是基于 region 的。G1根據各個Region回收所獲得的空間大小以及回收所需時間等指標在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大(垃圾)的Region,從而可以有計劃地避免在整個Java堆中進行全區域的垃圾收集。這也是 "Garbage First" 得名的由來。
G1從整體來看是基于“標記-整理”算法實現的收集器,但從局部(兩個Region之間)上看又是基于“標記-復制”算法實現,無論如何,這兩種算法都意味著G1運作期間不會產生內存空間碎片,垃圾收集完成之后能提供規整的可用內存。這種特性有利于程序長時間運行,在程序為大對象分配內存時不容易因無法找到連續內存空間而提前觸發下一次GC。
??? 提問環節:
一個對象和它內部所引用的對象可能不在同一個 Region 中,那么當垃圾回收時,是否需要掃描整個堆內存才能完整地進行一次可達性分析?
這里就需要引入?Remembered Set?的概念了。
答案是不需要,每個 Region 都有一個?Remembered Set (記憶集),用于記錄本區域中所有對象引用的對象所在的區域,進行可達性分析時,只要在 GC Roots 中再加上 Remembered Set 即可防止對整個堆內存進行遍歷。
再提一個概念,Collection Set?:簡稱 CSet,記錄了等待回收的 Region 集合,GC 時這些 Region 中的對象會被回收(copied or moved)。
G1 運作步驟
如果不計算維護 Remembered Set 的操作,G1 收集器的工作過程分為以下幾個步驟:
-
初始標記(Initial Marking):Stop The World,僅使用一條初始標記線程對所有與 GC Roots 直接關聯的對象進行標記。
-
并發標記(Concurrent Marking):使用一條標記線程與用戶線程并發執行。此過程進行可達性分析,速度很慢。
-
最終標記(Final Marking):Stop The World,使用多條標記線程并發執行。
-
篩選回收(Live Data Counting and Evacuation):回收廢棄對象,此時也要 Stop The World,并使用多條篩選回收線程并發執行。(還會更新Region的統計數據,對各個Region的回收價值和成本進行排序)
從上述階段的描述可以看出,G1收集器除了并發標記外,其余階段也是要完全暫停用戶線程的,換言之,它并非純粹地追求低延遲,官方給它設定的目標是在延遲可控的情況下獲得盡可能高的吞吐量
G1 的 Minor GC/Young GC
在分配一般對象時,當所有eden region使用達到最大閾值并且無法申請足夠內存時,會觸發一次YGC。每次YGC會回收所有Eden以及Survivor區,并且將存活對象復制到Old區以及另一部分的Survivor區。
下面是一段經過抽取的GC日志:
GC pause (G1 Evacuation Pause) (young) ├── Parallel Time ├── GC Worker Start ├── Ext Root Scanning ├── Update RS ├── Scan RS ├── Code Root Scanning ├── Object Copy ├── Code Root Fixup ├── Code Root Purge ├── Clear CT ├── Other ├── Choose CSet ├── Ref Proc ├── Ref Enq ├── Redirty Cards ├── Humongous Register ├── Humongous Reclaim ├── Free CSet由這段GC日志我們可知,整個YGC由多個子任務以及嵌套子任務組成,且一些核心任務為:Root Scanning,Update/Scan RS,Object Copy,CleanCT,Choose CSet,Ref Proc,Humongous Reclaim,Free CSet。
G1 的 Mixed GC
當越來越多的對象晉升到老年代Old Region 時,為了避免堆內存被耗盡,虛擬機會觸發一個混合的垃圾收集器,即Mixed GC,是收集整個新生代以及部分老年代的垃圾收集。除了回收整個Young Region,還會回收一部分的Old Region ,這里需要注意:是一部分老年代,而不是全部老年代,可以選擇哪些Old Region 進行收集,從而可以對垃圾回收的耗時時間進行控制。
Mixed GC的整個子任務和YGC完全一樣,只是回收的范圍不一樣。
注:G1 一般來說是沒有FGC的概念的。因為它本身不提供FGC的功能。
如果 Mixed GC 仍然效果不理想,跟不上新對象分配內存的需求,會使用 Serial Old GC 進行 Full GC強制收集整個 Heap。
相比CMS,G1總結有以下優點:
G1運作期間不會產生內存空間碎片,垃圾收集完成之后能提供規整的可用內存。這種特性有利于程序長時間運行。
G1 能預測 GC 停頓時間, STW 時間可控(G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.)
關于G1實際上還有很多的細節可以講,這里希望讀者去閱讀《深入理解Java虛擬機》或者其他資料來延伸學習,查漏補缺。
相關參數:
本系列關于JVM 垃圾回收的知識就到這里了。
因為篇幅的關系,也受限于能力水平,本文很多細節沒有涉及到,只能算是為學習JVM的同學打開了一扇的門(一扇和平常看到的文章相比要大那么一點點的門,寫了這么久允許我自戀一下吧😂😂)。希望不過癮的同學能自己更加深入的學習。
如果本文有幫助到你,希望能點個贊,這是對我的最大動力🤝🤝🤗🤗。
作者:Richard_Yi
https://juejin.im/post/5e151b38f265da5d495c8025
總結
以上是生活随笔為你收集整理的图文并茂,万字详解,带你掌握 JVM 垃圾回收!的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 我觉得有不少人被Spring带着跑偏了!
- 下一篇: 为什么我不再推荐使用 MVC 框架?