淺入淺出JVM - GC垃圾回收及垃圾收集器細談

垃圾回收機制(GC):

JVM在進行GC時,並非每次都對上面三個內存區域一起回收的,大部分時候回收的都是指新生代。 因此GC按照回收的區域又分了兩種類型,一種是普通GC(minor GC),一種是全局GC(major GC or Full GC)

Minor GC和Full GC的區別:

  • 普通GC(minor GC):只針對新生代區域的GC,指發生在新生代的垃圾收集動作,因爲大多數Java對象存活率都不高,所以Minor GC非常頻繁,一般回收速度也比較快。

  • 全局GC(major GC or Full GC):指發生在老年代的垃圾收集動作,出現了Major GC,經常會伴隨至少一次的Minor GC(但並不是絕對的)。Major GC的速度一般要比Minor GC慢上10倍以上 。

GC作用域:方法區和堆區,大多數情況下發生在堆區。

常見的垃圾回收算法:

  • 引用計數法 :JVM不推薦使用這種實現方式。缺點是:

  1. 每次對對象賦值時均要維護引用計數器,且計數器本身也有一定的消耗。

  2. 較難處理循環引用的問題。

  • 複製算法:用於新生代,產生MinorGC的過程(複製 -> 清空 -> 互換)

過程:

缺點:

  1. 它浪費了一半的內存,這太要命了。

  2. 如果對象的存活率很高,我們可以極端一點,假設是100%存活,那麼我們需要將所有對象都複製一遍,並將所有引用地址重置一遍。複製這一工作所花費的時間,在對象存活率達到一定程度時,將會變的不可忽視。 所以從以上描述不難看出,複製算法要想使用,最起碼對象的存活率要非常低纔行,而且最重要的是,我們必須要克服50%內存的浪費。

  • 標記清除法:老年代一般是由標記清除或者是標記清除與標記整理的混合實現。

過程:

用通俗的話解釋一下標記清除算法,就是當程序運行期間,若可以使用的內存被耗盡的時候,GC線程就會被觸發並將程序暫停,隨後將要回收的對象標記一遍,最終統一回收這些對象,完成標記清理工作接下來便讓應用程序恢復運行。

主要進行兩項工作,第一項則是標記,第二項則是清除。 標記:從引用根節點開始標記遍歷所有的GC Roots, 先標記出要回收的對象。 清除:遍歷整個堆,把標記的對象清除。

缺點:此算法需要暫停整個應用,會產生內存碎片 。

  1. 首先,它的缺點就是效率比較低(遞歸與全堆對象遍歷),而且在進行GC的時候,需要停止應用程序,這會導致用戶體驗非常差勁

  2. 其次,主要的缺點則是這種方式清理出來的空閒內存是不連續的,這點不難理解,我們的死亡對象都是隨即的出現在內存的各個角落的,現在把它們清除之後,內存的佈局自然會亂七八糟。而爲了應付這一點,JVM就不得不維持一個內存的空閒列表,這又是一種開銷。而且在分配數組對象的時候,尋找連續的內存空間會不太好找。

  • 標記壓縮法: 老年代一般是由標記清除或者是標記清除與標記整理的混合實現。

在整理壓縮階段,不再對標記的對像做回收,而是通過所有存活對像都向一端移動,然後直接清除邊界以外的內存。 可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。 標記/整理算法不僅可以彌補標記/清除算法當中,內存區域分散的缺點,也消除了複製算法當中,內存減半的高額代價。

缺點:

標記/整理算法唯一的缺點就是效率也不高,不僅要標記所有存活對象,還要整理所有存活對象的引用地址。 從效率上來說,標記/整理算法要低於複製算法。

整體對比:

  • 內存效率:複製算法>標記清除算法>標記整理算法(此處的效率只是簡單的對比時間複雜度,實際情況不一定如此)。

  • 內存整齊度:複製算法=標記整理算法>標記清除算法。

  • 內存利用率:標記整理算法=標記清除算法>複製算法。

GC Roots

要進行垃圾回收,需要判斷一個對象是否可以被回收,JVM是是通過枚舉根節點做可達性分析來實現這個過程的。

Java可以做GC ROOTS的對象:

  • 虛擬機棧(棧幀中的局部變量區,也叫做局部變量表;

  • 方法區中的類靜態屬性引用的對象;

  • 方法區中常量引用的對象;

  • 本地方法棧中N( Native方法)引用的對象。

四大引用 - 強、軟、弱、虛

強引用(Strong Reference):

Java中默認聲明的就是強引用,比如:

public class StrongRef {
    public static void main(String[] args) {
​
        Object obj = new Object(); //強引用賦值,只要obj還指向Object對象,Object對象就不會被回收
        Object obj1 = obj; //引用賦值
        obj = null; //置空
​
        System.gc();
        System.out.println(obj1);
    }
}

只要強引用存在,垃圾回收器將永遠不會回收被引用的對象,哪怕內存不足時,JVM也會直接拋出OutOfMemoryError,不會去回收。如果想中斷強引用與對象之間的聯繫,可以顯示的將強引用賦值爲null,這樣一來,JVM就可以適時的回收對象了。

軟引用(Soft Reference):

軟引用是用來描述一些非必需但仍有用的對象。在內存足夠的時候,軟引用對象不會被回收,只有在內存不足時,系統則會回收軟引用對象,如果回收了軟引用對象之後仍然沒有足夠的內存,纔會拋出內存溢出異常。這種特性常常被用來實現緩存技術,比如網頁緩存,圖片緩存等。在 JDK1.2 之後,用java.lang.ref.SoftReference類來表示軟引用。

/**
 * 內存夠用就保留,不夠就回收
 */
public class SoftRef {
    public static void main(String[] args) {
​
        Object obj = new Object();
        SoftReference<Object> softReference = new SoftReference(obj);
        System.out.println(obj);
        System.out.println(softReference.get());
​
        System.gc();
        System.out.println(obj);
        System.out.println(softReference.get());
    }
}

弱引用(Weak Reference):

弱引用的引用強度比軟引用要更弱一些,無論內存是否足夠,只要 JVM 開始進行垃圾回收,那些被弱引用關聯的對象都會被回收。在 JDK1.2 之後,用 java.lang.ref.WeakReference 來表示弱引用。

虛引用():

虛引用是最弱的一種引用關係,如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,它隨時可能會被回收,在 JDK1.2 之後,用 PhantomReference 類來表示,通過查看這個類的源碼,發現它只有一個構造函數和一個 get() 方法,而且它的 get() 方法僅僅是返回一個null,也就是說將永遠無法通過虛引用來獲取對象,虛引用必須要和 ReferenceQueue 引用隊列一起使用

引用隊列(ReferenceQueue):

引用隊列可以與軟引用、弱引用以及虛引用一起配合使用,當垃圾回收器準備回收一個對象時,如果發現它還有引用,那麼就會在回收對象之前,把這個引用加入到與之關聯的引用隊列中去。程序可以通過判斷引用隊列中是否已經加入了引用,來判斷被引用的對象是否將要被垃圾回收,這樣就可以在對象被回收之前採取一些必要的措施。

與軟引用、弱引用不同,虛引用必須和引用隊列一起使用。

GC收集器

GC算法(引用計數/複製/標清/標整)是內存回收的方法論,垃圾收集器就是算法落地實現。

四種主要的垃圾收集器:

串行垃圾回收器(Serial):它爲單線程環境設計並且只使用一個線程進行垃圾回收,會暫停所有的用戶線程。所以不適合服務器環境。

並行垃圾回收器(Parallel):多個垃圾回收線程並行工作,此時用戶線程是暫停的,適用於科學計算/大數據處理等弱交互場景。

併發垃圾回收器(CMS):用戶線程和垃圾收集線程同時執行(不一定是並行,可能交替執行),不需要停頓用戶線程 互聯網公司多用它,適用於對響應時間有要求的場景。

G1垃圾回收器:G1垃圾回收器將堆內存分割成不同的區域然後併發的對其進行垃圾回收。

查看當前使用的垃圾收集器:

java -XX:+PrintCommandLineFlags -version

默認的垃圾收集器有哪些:

GC日誌中部分參數解釋:

參數 解釋說明
DefNew Default New Generation
Tenured Old
ParNew Parallel New Generation
PSYoungGen Parallel Scavenge
ParOldGen Parallel Old Generation

新生代:

  1. 串行GC(Serial)/(Serial Coping)

    Serial收集器是一款非常古老的收集器,它使用單線程串行方式回收年輕代,會產生STW(Stop The World,即停止所有用戶線程,只有GC線程在運行)。

    每次進行GC時,首先停頓所有的用戶線程,然後只有一個GC線程回收年輕代中的死亡對象。在Java Client模式中,默認仍然使用Serial,因爲Client模式主要針對桌面應用,一般內存較小,在百M範圍內,使用單線程收集Serial效率非常高,可以帶來很少時間的停頓,用戶體驗非常好。

    對應JVM參數是:-XX:+UseSerialGC

    開啓後會使用Serial(年輕代) + Serial Old(老年代)的收集器組合。

    新生代使用複製算法,老年代使用標記-整理算法

  2. 並行GC(ParNew)

    ①:新生代使用ParNew收集器,可以看到有多條GC線程在進行垃圾回收,採用複製算法,會暫停其他用戶線程(STW)專心做垃圾回收。 ②:老年代使用Serial Old收集器,採用標記整理算法,會發生STW。

    對應JVM參數:-XX:+UseParNewGC

    開啓後會使用ParNew (新生代)+ Serial Old(老年代)的收集器組合。

    新生代使用複製算法,老年代使用標記-整理算法

    但是,ParNew + Tenured這樣的搭配在Java8中已經不再被推薦。

  3. 並行回收GC(Parallel)/(Parallel Scavenge)

    Parallel Scavenge 收集器也是新生代收集器,也是使用複製算法的多線程收集器。 看上去和ParNew收集器差不多,但是Parallel Scavenge最大的特點是更關注吞吐量。 吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值:

    吞吐量 = 運行用戶代碼時間 / (運行用戶代碼時間) + 垃圾收集時間

    打個比方,虛擬機運行了100分鐘,垃圾回收用了2分鐘,那麼吞吐量就是98%。 按照公式來看,吞吐量越高的虛擬機,自然是垃圾收集時間也越短,理所當然的用戶體驗也要更好。Parallel Scavenge收集器會根據當前系統的運行情況,動態調整某些參數來提供最合適的停頓時間或最大的吞吐量,這就是GC的自適應調節策略,這也是其與ParNew收集器最明顯的區別。

    對應JVM參數:-XX:+UseParallelGC 或 -XX:+UseParallelOldGC來使用Parallel Scavenge 收集器。

    開啓後會使用Parallel Scavenge(新生代)+ Parallel Old(老年代)的收集器組合。

    新生代使用複製算法,老年代使用標記-整理算法

老年代:

  1. 串行回收GC(Serial Old)/(Serial MSC)

    Serial Old是Serial的垃圾收集器老年代版本,同樣是單線程的垃圾收集器,使用標記-整理算法,這個收集器也主要是運行在Client得JVM默認的老年代垃圾收集器,在java8不能顯式的配置。

    在Server模式下,主要有兩個用途(JDK版本在8及以後):

    1. 在JDK1.5之前版本中與新生代的Parallel Scavenge 收集器搭配使用。(Parallel Scavenge + Serial Pld)

    2. 作爲老年代中使用CMS收集器的後備處理方案。

       

  2. 並行GC(Parallel Old)/(Parallel MSC)

    是Parallel Scavenge收集器的老年代版本,用於老年代的垃圾回收,但與Parallel Scavenge不同的是,它使用的是“標記-整理算法”。適用於注重於吞吐量及CPU資源敏感的場合。

    使用方式:-XX:+UseParallelOldGC,打開該收集器後,將使用Parallel Scavenge(年輕代)+Parallel Old(老年代)的組合進行GC。

  3. 併發標記清除GC(CMS)

     

    對應JVM參數:-XX:+UseConcMarkSweepGC 開啓該參數後會自動執行 -XX:+UseParNewGC

    開啓後會使用ParNew(新生代) + CMS(老年代) + Serial Old的收集器組合,Serial Old將作爲CMS出錯的後備收集器

     

    工作流程:

     

    • 初始標記(CMS initial mark)

    • 併發標記(CMS concurrent mark)和用戶線程一起:進行GC Roots跟蹤的過程,和用戶線程一起工作,不需要暫停工作線程。

    • 重新標記(CMS remark):爲了修正在併發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,仍然需要暫停所有的工作線程。由於併發標記時,用戶線程依然運行,因此在正式清理前再做修正。

    • 併發清除(CMS concurrent sweep)和用戶線程一起:爲了修正在併發標記期間,因用戶程序繼續運行而導致標記產生變動的那一部分對象的標記記錄,由於併發標記時,用戶線程依然運行,因此在正式清理前,再做修正。

    優點:

    併發收集低停頓

    缺點:

    併發執行,對CPU資源壓力大。由於併發運行,CMS在收集與應用線程同時進行時會增加對堆內存的佔用,也就是說,CMS必須要在老年代堆內存用盡之前完成垃圾回收,否則CMS回收失敗時,將觸發擔保機制,Serial Old收集器將會以STW的方式進行一次GC,從而造成較大停頓時間。

    採用的標記-清除算法會導致大量內存碎片。標記清楚算法無法整理空間碎片,老年代空間會隨着應用時長被逐漸耗盡,最後將不得不通過擔保機制對堆內存進行壓縮。CMS也提供了參數-XX:CMSFullGCsBeforeCompaction(默認0,即每次都進行內存整理)來指定多少次CMS收集之後,進行一次壓縮的Full GC。

     

    垃圾收集器應用場景分析

  • 單CPU或者小內存、單機程序:

    -XX:+UseSerialGC

  • 多CPU,需要大吞吐量,如後臺計算機應用

    -XX:+UseParallelGC 或者 -XX:+UseParallelOldGC

  • 多CPU,追求低停頓時間,需快速響應如互聯網應用

    -XX:+UseConcMarkSweepGC 或者 -XX:+UseParNewGC

如果你遇到了其他的問題或者你也和我一樣對技術充滿熱情, 歡迎隨時與我交流! wechat: s13037657871
 

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