自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

看完這篇垃圾回收,和面試官扯皮沒問題了

開發(fā) 開發(fā)工具
Java 相比 C/C++ 最顯著的特點(diǎn)便是引入了自動垃圾回收 (下文統(tǒng)一用 GC 指代自動垃圾回收),它解決了 C/C++ 最令人頭疼的內(nèi)存管理問題,讓程序員專注于程序本身,不用關(guān)心內(nèi)存回收這些惱人的問題。

 Java 相比 C/C++ 最顯著的特點(diǎn)便是引入了自動垃圾回收 (下文統(tǒng)一用 GC 指代自動垃圾回收),它解決了 C/C++ 最令人頭疼的內(nèi)存管理問題,讓程序員專注于程序本身,不用關(guān)心內(nèi)存回收這些惱人的問題,這也是 Java 能大行其道的重要原因之一,GC 真正讓程序員的生產(chǎn)力得到了釋放,但是程序員很難感知到它的存在,這就好比,我們吃完飯后在桌上放下餐盤即走,服務(wù)員會替你收拾好這些餐盤,你不會關(guān)心服務(wù)員什么時候來收,怎么收。

有人說既然 GC 已經(jīng)自動我們完成了清理,不了解 GC 貌似也沒啥問題。在大多數(shù)情況下確實沒問題,不過如果涉及到一些性能調(diào)優(yōu),問題排查等,深入地了解 GC 還是必不可少的,曾經(jīng)美團(tuán)通過調(diào)整 JVM 相關(guān) GC 參數(shù)讓服務(wù)響應(yīng)時間 TP90,TP99都下降了10ms+,服務(wù)可用性得到了很大的提升!所以深入了解 GC 是成為一名優(yōu)秀 Java 程序員的必修課!

垃圾回收分上下篇,上篇會先講垃圾回收理論,主要包括

GC 的幾種主要的收集方法:標(biāo)記清除、標(biāo)記整理、復(fù)制算法的原理與特點(diǎn),各自的優(yōu)劣勢

為啥會有 Serial ,CMS, G1 等各式樣的回收器,各自的優(yōu)劣勢是什么,為啥沒有一個統(tǒng)一的萬能的垃圾回收器

新生代為啥要設(shè)置成 Eden, S0,S1 這三個區(qū),基于什么考慮呢

堆外內(nèi)存不受 GC 控制,那該怎么釋放呢

對象可回收,就一定會被回收嗎?

什么是 SafePoint,什么是 Stop The World

下篇主要講垃圾回收的實踐,主要包括

GC 日志格式怎么看

主要有哪些發(fā)生 OOM 的場景

發(fā)生 OOM,如何定位,常用的內(nèi)存調(diào)試工具有哪些

本文會從以下幾方面來闡述垃圾回收

JVM 內(nèi)存區(qū)域

如何識別垃圾

引用計數(shù)法

可達(dá)性算法

垃圾回收主要方法

標(biāo)記清除法

復(fù)制法

標(biāo)記整理法

分代收集算法

垃圾回收器對比

文字比較多,不過也為了便于讀者理解加了不少 GC 的動畫,相信看完會有不少收獲

JVM 內(nèi)存區(qū)域

要搞懂垃圾回收的機(jī)制,我們首先要知道垃圾回收主要回收的是哪些數(shù)據(jù),這些數(shù)據(jù)主要在哪一塊區(qū)域,所以我們一起來看下 JVM 的內(nèi)存區(qū)域

 

 

 

 

虛擬機(jī)棧:描述的是方法執(zhí)行時的內(nèi)存模型,是線程私有的,生命周期與線程相同,每個方法被執(zhí)行的同時會創(chuàng)建棧楨(下文會看到),主要保存執(zhí)行方法時的局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址等信息,方法執(zhí)行時入棧,方法執(zhí)行完出棧,出棧就相當(dāng)于清空了數(shù)據(jù),入棧出棧的時機(jī)很明確,所以這塊區(qū)域不需要進(jìn)行 GC。

本地方法棧:與虛擬機(jī)棧功能非常類似,主要區(qū)別在于虛擬機(jī)棧為虛擬機(jī)執(zhí)行 Java 方法時服務(wù),而本地方法棧為虛擬機(jī)執(zhí)行本地方法時服務(wù)的。這塊區(qū)域也不需要進(jìn)行 GC

程序計數(shù)器:線程獨(dú)有的, 可以把它看作是當(dāng)前線程執(zhí)行的字節(jié)碼的行號指示器,比如如下字節(jié)碼內(nèi)容,在每個字節(jié)碼`前面都有一個數(shù)字(行號),我們可以認(rèn)為它就是程序計數(shù)器存儲的內(nèi)容

 

記錄這些數(shù)字(指令地址)有啥用呢,我們知道 Java 虛擬機(jī)的多線程是通過線程輪流切換并分配處理器的時間來完成的,在任何一個時刻,一個處理器只會執(zhí)行一個線程,如果這個線程被分配的時間片執(zhí)行完了(線程被掛起),處理器會切換到另外一個線程執(zhí)行,當(dāng)下次輪到執(zhí)行被掛起的線程(喚醒線程)時,怎么知道上次執(zhí)行到哪了呢,通過記錄在程序計數(shù)器中的行號指示器即可知道,所以程序計數(shù)器的主要作用是記錄線程運(yùn)行時的狀態(tài),方便線程被喚醒時能從上一次被掛起時的狀態(tài)繼續(xù)執(zhí)行,需要注意的是,程序計數(shù)器是唯一一個在 Java 虛擬機(jī)規(guī)范中沒有規(guī)定任何 OOM 情況的區(qū)域,所以這塊區(qū)域也不需要進(jìn)行 GC

 

 

本地內(nèi)存:線程共享區(qū)域,Java 8 中,本地內(nèi)存,也是我們通常說的堆外內(nèi)存,包含元空間和直接內(nèi)存,注意到上圖中 Java 8 和 Java 8 之前的 JVM 內(nèi)存區(qū)域的區(qū)別了嗎,在 Java 8 之前有個永久代的概念,實際上指的是 HotSpot 虛擬機(jī)上的永久代,它用永久代實現(xiàn)了 JVM 規(guī)范定義的方法區(qū)功能,主要存儲類的信息,常量,靜態(tài)變量,即時編譯器編譯后代碼等,這部分由于是在堆中實現(xiàn)的,受 GC 的管理,不過由于永久代有 -XX:MaxPermSize 的上限,所以如果動態(tài)生成類(將類信息放入永久代)或大量地執(zhí)行String.intern (將字段串放入永久代中的常量區(qū)),很容易造成 OOM,有人說可以把永久代設(shè)置得足夠大,但很難確定一個合適的大小,受類數(shù)量,常量數(shù)量的多少影響很大。所以在 Java 8 中就把方法區(qū)的實現(xiàn)移到了本地內(nèi)存中的元空間中,這樣方法區(qū)就不受 JVM 的控制了,也就不會進(jìn)行 GC,也因此提升了性能(發(fā)生 GC 會發(fā)生 Stop The Word,造成性能受到一定影響,后文會提到),也就不存在由于永久代限制大小而導(dǎo)致的 OOM 異常了(假設(shè)總內(nèi)存1G,JVM 被分配內(nèi)存 100M, 理論上元空間可以分配 2G-100M = 1.9G,空間大小足夠),也方便在元空間中統(tǒng)一管理。綜上所述,在 Java 8 以后這一區(qū)域也不需要進(jìn)行 GC

畫外音: 思考一個問題,堆外內(nèi)存不受 GC控制,無法通過 GC 釋放內(nèi)存,那該以什么樣的形式釋放呢,總不能只創(chuàng)建不釋放吧,這樣的話內(nèi)存可能很快就滿了,這里不做詳細(xì)闡述,請看文末的參考文章

堆:前面幾塊數(shù)據(jù)區(qū)域都不進(jìn)行 GC,那只剩下堆了,是的,這里是 GC 發(fā)生的區(qū)域!對象實例和數(shù)組都是在堆上分配的,GC 也主要對這兩類數(shù)據(jù)進(jìn)行回收,這塊也是我們之后重點(diǎn)需要分析的區(qū)域

如何識別垃圾

上一節(jié)我們詳細(xì)講述了 JVM 的內(nèi)存區(qū)域,知道了 GC 主要發(fā)生在堆,那么 GC 該怎么判斷堆中的對象實例或數(shù)據(jù)是不是垃圾呢,或者說判斷某些數(shù)據(jù)是否是垃圾的方法有哪些。

引用計數(shù)法

最容易想到的一種方式是引用計數(shù)法,啥叫引用計數(shù)法,簡單地說,就是對象被引用一次,在它的對象頭上加一次引用次數(shù),如果沒有被引用(引用次數(shù)為 0),則此對象可回收

String ref = new String("Java");

以上代碼 ref1 引用了右側(cè)定義的對象,所以引用次數(shù)是 1

 

 

 

 

如果在上述代碼后面添加一個 ref = null,則由于對象沒被引用,引用次數(shù)置為 0,由于不被任何變量引用,此時即被回收,動圖如下

 

 

 

 

看起來用引用計數(shù)確實沒啥問題了,不過它無法解決一個主要的問題:循環(huán)引用!啥叫循環(huán)引用

  1. public  class TestRC { 
  2.  
  3.     TestRC instance; 
  4.     public TestRC(String name) { 
  5.     } 
  6.  
  7.     public static  void main(String[] args) { 
  8.         // 第一步 
  9.     A a = new TestRC("a"); 
  10.     B b = new TestRC("b"); 
  11.  
  12.         // 第二步 
  13.     a.instance = b; 
  14.     b.instance = a; 
  15.  
  16.         // 第三步 
  17.     a = null
  18.     b = null
  19.     } 

 

按步驟一步步畫圖

 

 

 

 

到了第三步,雖然 a,b 都被置為 null 了,但是由于之前它們指向的對象互相指向了對方(引用計數(shù)都為 1),所以無法回收,也正是由于無法解決循環(huán)引用的問題,所以現(xiàn)代虛擬機(jī)都不用引用計數(shù)法來判斷對象是否應(yīng)該被回收。

可達(dá)性算法

現(xiàn)代虛擬機(jī)基本都是采用這種算法來判斷對象是否存活,可達(dá)性算法的原理是以一系列叫做 GC Root 的對象為起點(diǎn)出發(fā),引出它們指向的下一個節(jié)點(diǎn),再以下個節(jié)點(diǎn)為起點(diǎn),引出此節(jié)點(diǎn)指向的下一個結(jié)點(diǎn)。。。(這樣通過 GC Root 串成的一條線就叫引用鏈),直到所有的結(jié)點(diǎn)都遍歷完畢,如果相關(guān)對象不在任意一個以 GC Root 為起點(diǎn)的引用鏈中,則這些對象會被判斷為「垃圾」,會被 GC 回收。

 

 

 

 

如圖示,如果用可達(dá)性算法即可解決上述循環(huán)引用的問題,因為從GC Root 出發(fā)沒有到達(dá) a,b,所以 a,b 可回收

a, b 對象可回收,就一定會被回收嗎?并不是,對象的 finalize 方法給了對象一次垂死掙扎的機(jī)會,當(dāng)對象不可達(dá)(可回收)時,當(dāng)發(fā)生GC時,會先判斷對象是否執(zhí)行了 finalize 方法,如果未執(zhí)行,則會先執(zhí)行 finalize 方法,我們可以在此方法里將當(dāng)前對象與 GC Roots 關(guān)聯(lián),這樣執(zhí)行 finalize 方法之后,GC 會再次判斷對象是否可達(dá),如果不可達(dá),則會被回收,如果可達(dá),則不回收!

注意: finalize 方法只會被執(zhí)行一次,如果第一次執(zhí)行 finalize 方法此對象變成了可達(dá)確實不會回收,但如果對象再次被 GC,則會忽略 finalize 方法,對象會被回收!這一點(diǎn)切記!

那么這些 GC Roots 到底是什么東西呢,哪些對象可以作為 GC Root 呢,有以下幾類

虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象

方法區(qū)中類靜態(tài)屬性引用的對象

方法區(qū)中常量引用的對象

本地方法棧中 JNI(即一般說的 Native 方法)引用的對象

虛擬機(jī)棧中引用的對象

如下代碼所示,a 是棧幀中的本地變量,當(dāng) a = null 時,由于此時 a 充當(dāng)了 GC Root 的作用,a 與原來指向的實例 new Test() 斷開了連接,所以對象會被回收。

  1. public class Test { 
  2.     public static  void main(String[] args) { 
  3.     Test a = new Test(); 
  4.     a = null
  5.     } 

 

方法區(qū)中類靜態(tài)屬性引用的對象

如下代碼所示,當(dāng)棧幀中的本地變量 a = null 時,由于 a 原來指向的對象與 GC Root (變量 a) 斷開了連接,所以 a 原來指向的對象會被回收,而由于我們給 s 賦值了變量的引用,s 在此時是類靜態(tài)屬性引用,充當(dāng)了 GC Root 的作用,它指向的對象依然存活!

  1. public  class Test { 
  2.     public  static Test s; 
  3.     public static  void main(String[] args) { 
  4.     Test a = new Test(); 
  5.     a.s = new Test(); 
  6.     a = null
  7.     } 

 

方法區(qū)中常量引用的對象

如下代碼所示,常量 s 指向的對象并不會因為 a 指向的對象被回收而回收

  1. public  class Test { 
  2.     public  static  final Test s = new Test(); 
  3.         public static void main(String[] args) { 
  4.         Test a = new Test(); 
  5.         a = null
  6.         } 

 

本地方法棧中 JNI 引用的對象

這是簡單給不清楚本地方法為何物的童鞋簡單解釋一下:所謂本地方法就是一個 java 調(diào)用非 java 代碼的接口,該方法并非 Java 實現(xiàn)的,可能由 C 或 Python等其他語言實現(xiàn)的, Java 通過 JNI 來調(diào)用本地方法, 而本地方法是以庫文件的形式存放的(在 WINDOWS 平臺上是 DLL 文件形式,在 UNIX 機(jī)器上是 SO 文件形式)。通過調(diào)用本地的庫文件的內(nèi)部方法,使 JAVA 可以實現(xiàn)和本地機(jī)器的緊密聯(lián)系,調(diào)用系統(tǒng)級的各接口方法,還是不明白?見文末參考,對本地方法定義與使用有詳細(xì)介紹。

當(dāng)調(diào)用 Java 方法時,虛擬機(jī)會創(chuàng)建一個棧楨并壓入 Java 棧,而當(dāng)它調(diào)用的是本地方法時,虛擬機(jī)會保持 Java 棧不變,不會在 Java 棧禎中壓入新的禎,虛擬機(jī)只是簡單地動態(tài)連接并直接調(diào)用指定的本地方法。

 

 

 

 

 

  1. JNIEXPORT void JNICALL Java_com_pecuyu_jnirefdemo_MainActivity_newStringNative(JNIEnv *env, jobject instance,jstring jmsg) { 
  2. ... 
  3.    // 緩存String的class 
  4.    jclass jc = (*env)->FindClass(env, STRING_PATH); 

如上代碼所示,當(dāng) java 調(diào)用以上本地方法時,jc 會被本地方法棧壓入棧中, jc 就是我們說的本地方法棧中 JNI 的對象引用,因此只會在此本地方法執(zhí)行完成后才會被釋放。

垃圾回收主要方法

上一節(jié)我們知道了可以通過可達(dá)性算法來識別哪些數(shù)據(jù)是垃圾,那該怎么對這些垃圾進(jìn)行回收呢。主要有以下幾種方式方式

標(biāo)記清除算法

復(fù)制算法

標(biāo)記整理法

標(biāo)記清除算法

步驟很簡單

先根據(jù)可達(dá)性算法標(biāo)記出相應(yīng)的可回收對象(圖中黃色部分)

對可回收的對象進(jìn)行回收

 

操作起來確實很簡單,也不用做移動數(shù)據(jù)的操作,那有啥問題呢?仔細(xì)看上圖,沒錯,內(nèi)存碎片!假如我們想在上圖中的堆中分配一塊需要連續(xù)內(nèi)存占用 4M 或 5M 的區(qū)域,顯然是會失敗,怎么解決呢,如果能把上面未使用的 2M, 2M,1M 內(nèi)存能連起來就能連成一片可用空間為 5M 的區(qū)域即可,怎么做呢?

 

 

復(fù)制算法

把堆等分成兩塊區(qū)域, A 和 B,區(qū)域 A 負(fù)責(zé)分配對象,區(qū)域 B 不分配, 對區(qū)域 A 使用以上所說的標(biāo)記法把存活的對象標(biāo)記出來(下圖有誤無需清除),然后把區(qū)域 A 中存活的對象都復(fù)制到區(qū)域 B(存活對象都依次緊鄰排列)最后把 A 區(qū)對象全部清理掉釋放出空間,這樣就解決了內(nèi)存碎片的問題了。

 

 

 

 

不過復(fù)制算法的缺點(diǎn)很明顯,比如給堆分配了 500M 內(nèi)存,結(jié)果只有 250M 可用,空間平白無故減少了一半!這肯定是不能接受的!另外每次回收也要把存活對象移動到另一半,效率低下(我們可以想想刪除數(shù)組元素再把非刪除的元素往一端移,效率顯然堪憂)

標(biāo)記整理法

前面兩步和標(biāo)記清除法一樣,不同的是它在標(biāo)記清除法的基礎(chǔ)上添加了一個整理的過程 ,即將所有的存活對象都往一端移動,緊鄰排列(如圖示),再清理掉另一端的所有區(qū)域,這樣的話就解決了內(nèi)存碎片的問題。

 

 

 

 

但是缺點(diǎn)也很明顯:每進(jìn)一次垃圾清除都要頻繁地移動存活的對象,效率十分低下。

分代收集算法

分代收集算法整合了以上算法,綜合了這些算法的優(yōu)點(diǎn),最大程度避免了它們的缺點(diǎn),所以是現(xiàn)代虛擬機(jī)采用的首選算法,與其說它是算法,倒不是說它是一種策略,因為它是把上述幾種算法整合在了一起,為啥需要分代收集呢,來看一下對象的分配有啥規(guī)律

 

如圖示:縱軸代表已分配的字節(jié),而橫軸代表程序運(yùn)行時間

 

 

由圖可知,大部分的對象都很短命,都在很短的時間內(nèi)都被回收了(IBM 專業(yè)研究表明,一般來說,98% 的對象都是朝生夕死的,經(jīng)過一次 Minor GC 后就會被回收),所以分代收集算法根據(jù)對象存活周期的不同將堆分成新生代和老生代(Java8以前還有個永久代),默認(rèn)比例為 1 : 2,新生代又分為 Eden 區(qū), from Survivor 區(qū)(簡稱S0),to Survivor 區(qū)(簡稱 S1),三者的比例為 8: 1 : 1,這樣就可以根據(jù)新老生代的特點(diǎn)選擇最合適的垃圾回收算法,我們把新生代發(fā)生的 GC 稱為 Young GC(也叫 Minor GC),老年代發(fā)生的 GC 稱為 Old GC(也稱為 Full GC)。

 

 

 

 

畫外音:思考一下,新生代為啥要分這么多區(qū)?

那么分代垃圾收集是怎么工作的呢,我們一起來看看

分代收集工作原理

1、對象在新生代的分配與回收

由以上的分析可知,大部分對象在很短的時間內(nèi)都會被回收,對象一般分配在 Eden 區(qū)

 

 

 

 

當(dāng) Eden 區(qū)將滿時,觸發(fā) Minor GC

 

 

 

我們之前怎么說來著,大部分對象在短時間內(nèi)都會被回收, 所以經(jīng)過 Minor GC 后只有少部分對象會存活,它們會被移到 S0 區(qū)(這就是為啥空間大小 Eden: S0: S1 = 8:1:1, Eden 區(qū)遠(yuǎn)大于 S0,S1 的原因,因為在 Eden 區(qū)觸發(fā)的 Minor GC 把大部對象(接近98%)都回收了,只留下少量存活的對象,此時把它們移到 S0 或 S1 綽綽有余)同時對象年齡加一(對象的年齡即發(fā)生 Minor GC 的次數(shù)),最后把 Eden 區(qū)對象全部清理以釋放出空間,動圖如下

 

 

 

 

當(dāng)觸發(fā)下一次 Minor GC 時,會把 Eden 區(qū)的存活對象和 S0(或S1) 中的存活對象(S0 或 S1 中的存活對象經(jīng)過每次 Minor GC 都可能被回收)一起移到 S1(Eden 和 S0 的存活對象年齡+1), 同時清空 Eden 和 S0 的空間。

 

 

 

若再觸發(fā)下一次 Minor GC,則重復(fù)上一步,只不過此時變成了 從 Eden,S1 區(qū)將存活對象復(fù)制到 S0 區(qū),每次垃圾回收, S0, S1 角色互換,都是從 Eden ,S0(或S1) 將存活對象移動到 S1(或S0)。也就是說在 Eden 區(qū)的垃圾回收我們采用的是復(fù)制算法,因為在 Eden 區(qū)分配的對象大部分在 Minor GC 后都消亡了,只剩下極少部分存活對象(這也是為啥 Eden:S0:S1 默認(rèn)為 8:1:1 的原因),S0,S1 區(qū)域也比較小,所以最大限度地降低了復(fù)制算法造成的對象頻繁拷貝帶來的開銷。

2、對象何時晉升老年代

當(dāng)對象的年齡達(dá)到了我們設(shè)定的閾值,則會從S0(或S1)晉升到老年代

 

如圖示:年齡閾值設(shè)置為 15, 當(dāng)發(fā)生下一次 Minor GC 時,S0 中有個對象年齡達(dá)到 15,達(dá)到我們的設(shè)定閾值,晉升到老年代!

 

 

大對象 當(dāng)某個對象分配需要大量的連續(xù)內(nèi)存時,此時對象的創(chuàng)建不會分配在 Eden 區(qū),會直接分配在老年代,因為如果把大對象分配在 Eden 區(qū), Minor GC 后再移動到 S0,S1 會有很大的開銷(對象比較大,復(fù)制會比較慢,也占空間),也很快會占滿 S0,S1 區(qū),所以干脆就直接移到老年代.

還有一種情況也會讓對象晉升到老年代,即在 S0(或S1) 區(qū)相同年齡的對象大小之和大于 S0(或S1)空間一半以上時,則年齡大于等于該年齡的對象也會晉升到老年代。

3、空間分配擔(dān)保

在發(fā)生 MinorGC 之前,虛擬機(jī)會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象的總空間,如果大于,那么Minor GC 可以確保是安全的,如果不大于,那么虛擬機(jī)會查看 HandlePromotionFailure 設(shè)置值是否允許擔(dān)保失敗。如果允許,那么會繼續(xù)檢查老年代最大可用連續(xù)空間是否大于歷次晉升到老年代對象的平均大小,如果大于則進(jìn)行 Minor GC,否則可能進(jìn)行一次 Full GC。

4、Stop The World

如果老年代滿了,會觸發(fā) Full GC, Full GC 會同時回收新生代和老年代(即對整個堆進(jìn)行GC),它會導(dǎo)致 Stop The World(簡稱 STW),造成挺大的性能開銷。

什么是 STW ?所謂的 STW, 即在 GC(minor GC 或 Full GC)期間,只有垃圾回收器線程在工作,其他工作線程則被掛起。

 

 

 

 

畫外音:為啥在垃圾收集期間其他工作線程會被掛起?想象一下,你一邊在收垃圾,另外一群人一邊丟垃圾,垃圾能收拾干凈嗎。

一般 Full GC 會導(dǎo)致工作線程停頓時間過長(因為Full GC 會清理整個堆中的不可用對象,一般要花較長的時間),如果在此 server 收到了很多請求,則會被拒絕服務(wù)!所以我們要盡量減少 Full GC(Minor GC 也會造成 STW,但只會觸發(fā)輕微的 STW,因為 Eden 區(qū)的對象大部分都被回收了,只有極少數(shù)存活對象會通過復(fù)制算法轉(zhuǎn)移到 S0 或 S1 區(qū),所以相對還好)。

現(xiàn)在我們應(yīng)該明白把新生代設(shè)置成 Eden, S0,S1區(qū)或者給對象設(shè)置年齡閾值或者默認(rèn)把新生代與老年代的空間大小設(shè)置成 1:2 都是為了盡可能地避免對象過早地進(jìn)入老年代,盡可能晚地觸發(fā) Full GC。想想新生代如果只設(shè)置 Eden 會發(fā)生什么,后果就是每經(jīng)過一次 Minor GC,存活對象會過早地進(jìn)入老年代,那么老年代很快就會裝滿,很快會觸發(fā) Full GC,而對象其實在經(jīng)過兩三次的 Minor GC 后大部分都會消亡,所以有了 S0,S1的緩沖,只有少數(shù)的對象會進(jìn)入老年代,老年代大小也就不會這么快地增長,也就避免了過早地觸發(fā) Full GC。

由于 Full GC(或Minor GC) 會影響性能,所以我們要在一個合適的時間點(diǎn)發(fā)起 GC,這個時間點(diǎn)被稱為 Safe Point,這個時間點(diǎn)的選定既不能太少以讓 GC 時間太長導(dǎo)致程序過長時間卡頓,也不能過于頻繁以至于過分增大運(yùn)行時的負(fù)荷。一般當(dāng)線程在這個時間點(diǎn)上狀態(tài)是可以確定的,如確定 GC Root 的信息等,可以使 JVM 開始安全地 GC。Safe Point 主要指的是以下特定位置:

循環(huán)的末尾

方法返回前

調(diào)用方法的 call 之后

拋出異常的位置 另外需要注意的是由于新生代的特點(diǎn)(大部分對象經(jīng)過 Minor GC后會消亡), Minor GC 用的是復(fù)制算法,而在老生代由于對象比較多,占用的空間較大,使用復(fù)制算法會有較大開銷(復(fù)制算法在對象存活率較高時要進(jìn)行多次復(fù)制操作,同時浪費(fèi)一半空間)所以根據(jù)老生代特點(diǎn),在老年代進(jìn)行的 GC 一般采用的是標(biāo)記整理法來進(jìn)行回收。

垃圾收集器種類

如果說收集算法是內(nèi)存回收的方法論,那么垃圾收集器就是內(nèi)存回收的具體實現(xiàn)。Java 虛擬機(jī)規(guī)范并沒有規(guī)定垃圾收集器應(yīng)該如何實現(xiàn),因此一般來說不同廠商,不同版本的虛擬機(jī)提供的垃圾收集器實現(xiàn)可能會有差別,一般會給出參數(shù)來讓用戶根據(jù)應(yīng)用的特點(diǎn)來組合各個年代使用的收集器,主要有以下垃圾收集器

 

 

 

 

在新生代工作的垃圾回收器:Serial, ParNew, ParallelScavenge

在老年代工作的垃圾回收器:CMS,Serial Old, Parallel Old

同時在新老生代工作的垃圾回收器:G1

圖片中的垃圾收集器如果存在連線,則代表它們之間可以配合使用,接下來我們來看看各個垃圾收集器的具體功能。

新生代收集器

Serial 收集器

Serial 收集器是工作在新生代的,單線程的垃圾收集器,單線程意味著它只會使用一個 CPU 或一個收集線程來完成垃圾回收,不僅如此,還記得我們上文提到的 STW 了嗎,它在進(jìn)行垃圾收集時,其他用戶線程會暫停,直到垃圾收集結(jié)束,也就是說在 GC 期間,此時的應(yīng)用不可用。

看起來單線程垃圾收集器不太實用,不過我們需要知道的任何技術(shù)的使用都不能脫離場景,在Client 模式下,它簡單有效(與其他收集器的單線程比),對于限定單個 CPU 的環(huán)境來說,Serial 單線程模式無需與其他線程交互,減少了開銷,專心做 GC 能將其單線程的優(yōu)勢發(fā)揮到極致,另外在用戶的桌面應(yīng)用場景,分配給虛擬機(jī)的內(nèi)存一般不會很大,收集幾十甚至一兩百兆(僅是新生代的內(nèi)存,桌面應(yīng)用基本不會再大了),STW 時間可以控制在一百多毫秒內(nèi),只要不是頻繁發(fā)生,這點(diǎn)停頓是可以接受的,所以對于運(yùn)行在 Client 模式下的虛擬機(jī),Serial 收集器是新生代的默認(rèn)收集器

ParNew 收集器

ParNew 收集器是 Serial 收集器的多線程版本,除了使用多線程,其他像收集算法,STW,對象分配規(guī)則,回收策略與 Serial 收集器完成一樣,在底層上,這兩種收集器也共用了相當(dāng)多的代碼,它的垃圾收集過程如下

 

 

 

ParNew 主要工作在 Server 模式,我們知道服務(wù)端如果接收的請求多了,響應(yīng)時間就很重要了,多線程可以讓垃圾回收得更快,也就是減少了 STW 時間,能提升響應(yīng)時間,所以是許多運(yùn)行在 Server 模式下的虛擬機(jī)的首選新生代收集器,另一個與性能無關(guān)的原因是因為除了 Serial 收集器,只有它能與 CMS 收集器配合工作,CMS 是一個劃時代的垃圾收集器,是真正意義上的并發(fā)收集器,它第一次實現(xiàn)了垃圾收集線程與用戶線程(基本上)同時工作,它采用的是傳統(tǒng)的 GC 收集器代碼框架,與 Serial,ParNew 共用一套代碼框架,所以能與這兩者一起配合工作,而后文提到的 Parallel Scavenge 與 G1 收集器沒有使用傳統(tǒng)的 GC 收集器代碼框架,而是另起爐灶獨(dú)立實現(xiàn)的,另外一些收集器則只是共用了部分的框架代碼,所以無法與 CMS 收集器一起配合工作。

在多 CPU 的情況下,由于 ParNew 的多線程回收特性,毫無疑問垃圾收集會更快,也能有效地減少 STW 的時間,提升應(yīng)用的響應(yīng)速度。

Parallel Scavenge 收集器

Parallel Scavenge 收集器也是一個使用復(fù)制算法,多線程,工作于新生代的垃圾收集器,看起來功能和 ParNew 收集器一樣,它有啥特別之處嗎

關(guān)注點(diǎn)不同,CMS 等垃圾收集器關(guān)注的是盡可能縮短垃圾收集時用戶線程的停頓時間,而 Parallel Scavenge 目標(biāo)是達(dá)到一個可控制的吞吐量(吞吐量 = 運(yùn)行用戶代碼時間 / (運(yùn)行用戶代碼時間+垃圾收集時間)),也就是說 CMS 等垃圾收集器更適合用到與用戶交互的程序,因為停頓時間越短,用戶體驗越好,而 Parallel Scavenge 收集器關(guān)注的是吞吐量,所以更適合做后臺運(yùn)算等不需要太多用戶交互的任務(wù)。

Parallel Scavenge 收集器提供了兩個參數(shù)來精確控制吞吐量,分別是控制最大垃圾收集時間的 -XX:MaxGCPauseMillis 參數(shù)及直接設(shè)置吞吐量大小的 -XX:GCTimeRatio(默認(rèn)99%)

除了以上兩個參數(shù),還可以用 Parallel Scavenge 收集器提供的第三個參數(shù) -XX:UseAdaptiveSizePolicy,開啟這個參數(shù)后,就不需要手工指定新生代大小,Eden 與 Survivor 比例(SurvivorRatio)等細(xì)節(jié),只需要設(shè)置好基本的堆大小(-Xmx 設(shè)置最大堆),以及最大垃圾收集時間與吞吐量大小,虛擬機(jī)就會根據(jù)當(dāng)前系統(tǒng)運(yùn)行情況收集監(jiān)控信息,動態(tài)調(diào)整這些參數(shù)以盡可能地達(dá)到我們設(shè)定的最大垃圾收集時間或吞吐量大小這兩個指標(biāo)。自適應(yīng)策略也是 Parallel Scavenge 與 ParNew 的重要區(qū)別!

老年代收集器

Serial Old 收集器

上文我們知道, Serial 收集器是工作于新生代的單線程收集器,與之相對地,Serial Old 是工作于老年代的單線程收集器,此收集器的主要意義在于給 Client 模式下的虛擬機(jī)使用,如果在 Server 模式下,則它還有兩大用途:一種是在 JDK 1.5 及之前的版本中與 Parallel Scavenge 配合使用,另一種是作為 CMS 收集器的后備預(yù)案,在并發(fā)收集發(fā)生 Concurrent Mode Failure 時使用(后文講述),它與 Serial 收集器配合使用示意圖如下

 

 

 

 

Parallel Old 收集器

Parallel Old 是相對于 Parallel Scavenge 收集器的老年代版本,使用多線程和標(biāo)記整理法,兩者組合示意圖如下,這兩者的組合由于都是多線程收集器,真正實現(xiàn)了「吞吐量優(yōu)先」的目標(biāo)

 

 

 

 

CMS 收集器

CMS 收集器是以實現(xiàn)最短 STW 時間為目標(biāo)的收集器,如果應(yīng)用很重視服務(wù)的響應(yīng)速度,希望給用戶最好的體驗,則 CMS 收集器是個很不錯的選擇!

我們之前說老年代主要用標(biāo)記整理法,而 CMS 雖然工作于老年代,但采用的是標(biāo)記清除法,主要有以下四個步驟

初始標(biāo)記

并發(fā)標(biāo)記

重新標(biāo)記

并發(fā)清除

 

 

 

 

從圖中可以的看到初始標(biāo)記和重新標(biāo)記兩個階段會發(fā)生 STW,造成用戶線程掛起,不過初始標(biāo)記僅標(biāo)記 GC Roots 能關(guān)聯(lián)的對象,速度很快,并發(fā)標(biāo)記是進(jìn)行 GC Roots Tracing 的過程,重新標(biāo)記是為了修正并發(fā)標(biāo)記期間因用戶線程繼續(xù)運(yùn)行而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,這一階段停頓時間一般比初始標(biāo)記階段稍長,但遠(yuǎn)比并發(fā)標(biāo)記時間短。

整個過程中耗時最長的是并發(fā)標(biāo)記和標(biāo)記清理,不過這兩個階段用戶線程都可工作,所以不影響應(yīng)用的正常使用,所以總體上看,可以認(rèn)為 CMS 收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的。

但是 CMS 收集器遠(yuǎn)達(dá)不到完美的程度,主要有以下三個缺點(diǎn)

CMS 收集器對 CPU 資源非常敏感 原因也可以理解,比如本來我本來可以有 10 個用戶線程處理請求,現(xiàn)在卻要分出 3 個作為回收線程,吞吐量下降了30%,CMS 默認(rèn)啟動的回收線程數(shù)是 (CPU數(shù)量+3)/ 4, 如果 CPU 數(shù)量只有一兩個,那吞吐量就直接下降 50%,顯然是不可接受的

CMS 無法處理浮動垃圾(Floating Garbage),可能出現(xiàn) 「Concurrent Mode Failure」而導(dǎo)致另一次 Full GC 的產(chǎn)生,由于在并發(fā)清理階段用戶線程還在運(yùn)行,所以清理的同時新的垃圾也在不斷出現(xiàn),這部分垃圾只能在下一次 GC 時再清理掉(即浮云垃圾),同時在垃圾收集階段用戶線程也要繼續(xù)運(yùn)行,就需要預(yù)留足夠多的空間要確保用戶線程正常執(zhí)行,這就意味著 CMS 收集器不能像其他收集器一樣等老年代滿了再使用,JDK 1.5 默認(rèn)當(dāng)老年代使用了68%空間后就會被激活,當(dāng)然這個比例可以通過 -XX:CMSInitiatingOccupancyFraction 來設(shè)置,但是如果設(shè)置地太高很容易導(dǎo)致在 CMS 運(yùn)行期間預(yù)留的內(nèi)存無法滿足程序要求,會導(dǎo)致 Concurrent Mode Failure 失敗,這時會啟用 Serial Old 收集器來重新進(jìn)行老年代的收集,而我們知道 Serial Old 收集器是單線程收集器,這樣就會導(dǎo)致 STW 更長了。

CMS 采用的是標(biāo)記清除法,上文我們已經(jīng)提到這種方法會產(chǎn)生大量的內(nèi)存碎片,這樣會給大內(nèi)存分配帶來很大的麻煩,如果無法找到足夠大的連續(xù)空間來分配對象,將會觸發(fā) Full GC,這會影響應(yīng)用的性能。當(dāng)然我們可以開啟 -XX:+UseCMSCompactAtFullCollection(默認(rèn)是開啟的),用于在 CMS 收集器頂不住要進(jìn)行 FullGC 時開啟內(nèi)存碎片的合并整理過程,內(nèi)存整理會導(dǎo)致 STW,停頓時間會變長,還可以用另一個參數(shù) -XX:CMSFullGCsBeforeCompation 用來設(shè)置執(zhí)行多少次不壓縮的 Full GC 后跟著帶來一次帶壓縮的。

G1(Garbage First) 收集器

G1 收集器是面向服務(wù)端的垃圾收集器,被稱為駕馭一切的垃圾回收器,主要有以下幾個特點(diǎn)

像 CMS 收集器一樣,能與應(yīng)用程序線程并發(fā)執(zhí)行。

整理空閑空間更快。

需要 GC 停頓時間更好預(yù)測。

不會像 CMS 那樣犧牲大量的吞吐性能。

不需要更大的 Java Heap

與 CMS 相比,它在以下兩個方面表現(xiàn)更出色

運(yùn)作期間不會產(chǎn)生內(nèi)存碎片,G1 從整體上看采用的是標(biāo)記-整理法,局部(兩個 Region)上看是基于復(fù)制算法實現(xiàn)的,兩個算法都不會產(chǎn)生內(nèi)存碎片,收集后提供規(guī)整的可用內(nèi)存,這樣有利于程序的長時間運(yùn)行。

在 STW 上建立了可預(yù)測的停頓時間模型,用戶可以指定期望停頓時間,G1 會將停頓時間控制在用戶設(shè)定的停頓時間以內(nèi)。

為什么G1能建立可預(yù)測的停頓模型呢,主要原因在于 G1 對堆空間的分配與傳統(tǒng)的垃圾收集器不一器,傳統(tǒng)的內(nèi)存分配就像我們前文所述,是連續(xù)的,分成新生代,老年代,新生代又分 Eden,S0,S1,如下

 

 

 

 

而 G1 各代的存儲地址不是連續(xù)的,每一代都使用了 n 個不連續(xù)的大小相同的 Region,每個Region占有一塊連續(xù)的虛擬內(nèi)存地址,如圖示

 

 

除了和傳統(tǒng)的新老生代,幸存區(qū)的空間區(qū)別,Region還多了一個H,它代表Humongous,這表示這些Region存儲的是巨大對象(humongous object,H-obj),即大小大于等于region一半的對象,這樣超大對象就直接分配到了老年代,防止了反復(fù)拷貝移動。那么 G1 分配成這樣有啥好處呢?

 

 

傳統(tǒng)的收集器如果發(fā)生 Full GC 是對整個堆進(jìn)行全區(qū)域的垃圾收集,而分配成各個 Region 的話,方便 G1 跟蹤各個 Region 里垃圾堆積的價值大小(回收所獲得的空間大小及回收所需經(jīng)驗值),這樣根據(jù)價值大小維護(hù)一個優(yōu)先列表,根據(jù)允許的收集時間,優(yōu)先收集回收價值最大的 Region,也就避免了整個老年代的回收,也就減少了 STW 造成的停頓時間。同時由于只收集部分 Region,可就做到了 STW 時間的可控。

G1 收集器的工作步驟如下

初始標(biāo)記

并發(fā)標(biāo)記

最終標(biāo)記

篩選回收

 

 

 

 

可以看到整體過程與 CMS 收集器非常類似,篩選階段會根據(jù)各個 Region 的回收價值和成本進(jìn)行排序,根據(jù)用戶期望的 GC 停頓時間來制定回收計劃。

總結(jié)

本文簡述了垃圾回收的原理與垃圾收集器的種類,相信大家對開頭提的一些問題應(yīng)該有了更深刻的認(rèn)識,在生產(chǎn)環(huán)境中我們要根據(jù)不同的場景來選擇垃圾收集器組合,如果是運(yùn)行在桌面環(huán)境處于 Client 模式的,則用 Serial + Serial Old 收集器綽綽有余,如果需要響應(yīng)時間快,用戶體驗好的,則用 ParNew + CMS 的搭配模式,即使是號稱是「駕馭一切」的 G1,也需要根據(jù)吞吐量等要求適當(dāng)調(diào)整相應(yīng)的 JVM 參數(shù),沒有最牛的技術(shù),只有最合適的使用場景,切記!

理論有了,下一篇我們會進(jìn)入手動操作環(huán)節(jié),我們會一起來動手操作一些 demo,做一些實驗,來驗證我們看到的一些現(xiàn)象,比如對象一般分配在新生代,什么情況下會直接到老年代,該怎么實驗?發(fā)生了OOM,該用哪些工具調(diào)試呢?等等,敬請期待!

責(zé)任編輯:武曉燕 來源: 碼海
相關(guān)推薦

2020-03-14 09:17:55

HTTPS網(wǎng)絡(luò)協(xié)議HTTP

2020-04-07 01:04:18

SessionCookieToken

2020-04-15 12:24:55

Exception Error Java

2020-11-02 08:12:52

finalJava開發(fā)

2020-01-15 08:06:28

HTTP超文本傳輸協(xié)議網(wǎng)絡(luò)協(xié)議

2020-05-15 11:14:58

操作系統(tǒng)面試官運(yùn)行

2019-05-31 15:30:00

人工智能機(jī)器人互聯(lián)網(wǎng)

2021-12-02 18:20:25

算法垃圾回收

2020-12-10 08:43:17

垃圾回收JVM

2018-04-23 11:00:44

PythonRedisNoSQL

2021-05-08 07:53:33

面試線程池系統(tǒng)

2021-02-03 15:30:10

面試垃圾回收器前端

2018-04-27 14:46:07

面試簡歷程序員

2019-10-10 11:20:22

MySQL索引數(shù)據(jù)庫

2021-04-30 00:00:50

Semaphore信號量面試官

2020-04-03 14:05:10

面試RedisJava

2009-06-25 17:48:24

Java垃圾回收

2019-04-15 14:40:46

消息隊列Java編程

2019-04-26 14:12:19

MySQL數(shù)據(jù)庫隔離級別

2020-04-20 08:35:48

HTTP HTTPS網(wǎng)絡(luò)協(xié)議
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號