【JVM】垃圾收集

一、垃圾收集算法

1. 標記-清除(Mark-Sweep)

算法分爲兩個階段:首先標記出所需要回收的對象,在標記完成後統一回收所有被標記的對象

1.1 不足
  1. 效率問題,標記和清除兩個過程效率都不高;
  2. 空間問題,標記清除之後會產生大量的連續的內存碎片,空間碎片太多會導致以後在程序運行中需要分配較大對象時,無法找到足夠的連續內存而不得提前觸發另一次垃圾收集動作

標記清除

2. 複製算法(Copying)

爲了解決效率問題,一種被稱爲複製的收集算法出現了,它將可用內存按容量劃分爲大小相等的兩份,每次只是用其中的一塊,當這一塊內存使用完了,就將還存活的這對象複製到另外一塊上面,然後再把已使用過的內存空間一次清理掉
在這裏插入圖片描述
複製算法

3. 標記整理算法

複製收集算法在對象存活率較高時就要進行較多的複製操作,效率將會變低;更關鍵的是,如果不想浪費50%的空間,就需要額外的空間進行分配擔保,以應對被使用的內存中所有對象都100%存活的極端,所以老年代一般不會使用此方法

標記-整理算法於標記清除算法的標記過程相同,但後續步驟不是直接清除,而是讓所有的對象都向一端移動,然後直接清理掉端邊界以外的內存;示意圖圖小:
標記整理

4. 分代收集算法

一般是把Java堆 分爲新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集算法。在新生代 中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就選用複製算法,只需要付 出少量存活對象的複製成本就可以完成收集。而老年代中因爲對象存活率高、沒有額外空間 對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”算法來進行回收。


二、收集算法的落地實現-收集器

垃圾收集器組合

1. Serial收集器

Serial [ˈsɪriəl] 連續的

Serial收集器是基本、發展歷史悠久的收集器,曾經(在JDK1.3.1之前)是虛擬機新生代收集的唯一選擇。 它是一種單線程收集器,不僅僅意味着它只會使用一個CPU或者一條收集線程去完成垃圾收集工作,更重要的是其 在進行垃圾收集的時候需要暫停其他線程。

優點:簡單高效,擁有很高的單線程收集效率

缺點:收集過程需要暫停所有線程

算法:複製算法

適用範圍:新生代 應用:Client模式下的默認新生代收集器
在這裏插入圖片描述

2. ParNew收集器

Serial收集器的多線程版本

優點:在多CPU時,比Serial效率高。

缺點:收集過程暫停所有應用程序線程,單CPU時比Serial效率差

算法:複製算法 適用範圍:新生代

應用:運行在Server模式下的虛擬機中首選的新生代收集器

並行(Parallel):指多條垃圾收集器線程並行工作,但此時用戶線程任然處於等待狀態
併發 (Concurrent):用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行),用戶程序在繼續運行,而垃圾收集器程序運行在另一個CPU上

3. Parallel Scanvenge

Parallel Scavenge收集器是一個新生代收集器,它也是使用複製算法的收集器,又是並行的多線程收集器,看上去 和ParNew一樣,但是Parallel Scanvenge更關注 系統的吞吐量 。

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

  -XX:MaxGCPauseMills  #控制最大垃圾收集停頓時間
  -XX:GCRatio  #直接設置吞吐量的大小

4. Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一個單線程收集器,不同的是採用"標記-整理算法",運行過程 和Serial收集器一樣。

在這裏插入圖片描述

5. Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多線程和"標記-整理算法"進行垃圾回收。

關注點:吞吐量優先

在這裏插入圖片描述

6. CMS收集器

CMS(Concurrent Mark Sweep) 收集器是一種以獲取最短回收停頓時間爲目標的收集器,採用的是標記-清除算法

執行步驟步驟:

  1. 初始標記(CMS initial mark) 標記GC Roots能關聯到的對象 Stop The World —速度和很快

  2. 併發標記(CMS concurrent mark) 進行GC Roots Tracing

  3. 重新標記(CMS remark) 修改併發標記因用戶程序變動的內容 Stop The World

  4. 併發清除(CMS concurrent weep)

    由於整個過程中,併發標記和併發清除,收集器線程可以與用戶線程一起工作,所以總體上來說,CMS收集 器的內存回收過程是與用戶線程一起併發地執行的。

優點: 併發收集、低停頓

缺點:產生大量的空間碎片,併發階段會降低吞吐量;無法處理浮動垃圾
在這裏插入圖片描述

浮動垃圾:用於併發清理階段用戶程序還在運行着,伴隨程序的運行自然就還會產生新的垃圾,這一部分垃圾出現在標記之後,CMS無法在當次收集中處理他們,只能等待下一次GC是處理掉,這一部分垃圾被稱爲“浮動垃圾"

因爲浮動垃圾的存在,就需要預留多餘的空間給線程使用,CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿才進行垃圾收集;
當沒有足夠的空間時,就會出現“Concurrent Mode Failure”失敗,虛擬機將啓動後被預案,臨時啓用Serial Old收集器來 重新進行老年代的垃圾收集

  -XX:CMSInitiatingOccupancyFraction   #設置觸發垃圾回收百分比

7. G1收集器

G1是一款面向服務端應用的垃圾收集器;與其他收集器相比具備一下特點

併發與並行:

G1能充分利用多CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者 CPU核心)來縮短Stop-The-World停頓的時間,部分其他收集器原本需要停頓Java線程執行的 GC動作,G1收集器仍然可以通過併發的方式讓Java程序繼續執行

分代收集:

與其他收集器一樣,分代概念在G1中依然得以保留。雖然G1可以不需要其 他收集器配合就能獨立管理整個GC堆,但它能夠採用不同的方式去處理新創建的對象和已 經存活了一段時間、熬過多次GC的舊對象以獲取更好的收集效果。

空間整合:

與CMS的“標記—清理”算法不同,G1從整體來看是基於“標記—整理”算法實 現的收集器,從局部(兩個Region之間)上來看是基於“複製”算法實現的,但無論如何,這 兩種算法都意味着G1運作期間不會產生內存空間碎片,收集後能提供規整的可用內存。這種 特性有利於程序長時間運行,分配大對象時不會因爲無法找到連續內存空間而提前觸發下一 次GC。

可預測的停頓:

這是G1相對於CMS的另一大優勢,降低停頓時間是G1和CMS共同的關 注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一 個長度爲M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒,這幾乎已經是實 時Java(RTSJ)的垃圾收集器的特徵了。

G1收集器之所以能建立可預測的停頓時間模型,是因爲它可以有計劃地避免在整個Java 堆中進行全區域的垃圾收集。G1跟蹤各個Region裏面的垃圾堆積的價值大小(回收所獲得的 空間大小以及回收所需時間的經驗值),在後臺維護一個優先列表,每次根據允許的收集時 間,優先回收價值最大的Region(這也就是Garbage-First名稱的來由)。這種使用Region劃分 內存空間以及有優先級的區域回收方式,保證了G1收集器在有限的時間內可以獲取儘可能高 的收集效率。

使用G1收集器時,Java堆的內存佈局與就與其他收集器有很大差別,它將整個Java堆劃分爲多個大小相等的 獨立區域(Region),雖然還保留有新生代和老年代的概念,但新生代和老年代不再是物理隔離的了,它們 都是一部分Region(不需要連續)的集合。

運行過程

初始標記(Initial Marking):標記以下GC Root

併發標記(Concurrent Marking)

最終標記(Final Marking)

篩選回收(Live Data Counting and Evacuation)
在這裏插入圖片描述

三、垃圾收集器的分類

  1. 串行收集器

    Serial 和Serial Old,只能一個垃圾回收線程執行,用戶線程暫停,適用於內存比較小的嵌入式設備

  2. 並行收集器

    吞吐量優先,Parallel Scanvenge、Parallel Old,多線程垃圾收集器並行工作,但此時用戶線程仍然處於等待狀態;適用於科學計算、後臺處理等交互場景

  3. 併發收集器
    CMS、G1 收集器;用戶線程和垃圾收集線程同時執行(但不一定是並行,可能是交互執行),垃圾收集線程在執行的時候不會停頓用戶線程的運行,適用於對時間有要求的場景,如:web

四、垃圾收集的時機

GC是由JVM自動完成的,根據JVM系統系統環境而定,所以時機是不確定的;當然,我們可以手動進行垃圾回收,比如調用System.gc()方法通知JVM進行幾次垃圾回收,但是具體什麼時候運行也無法控制,也就是說System.gc()只是通知要回收,什麼時候回收,由JVM決定

一般一下幾種情況會發生垃圾回收

  1. 當Eden區或者S區不夠用了
  2. 當老年代空間不夠用了
  3. 方法區空間不夠用了
  4. System.gc();

五、堆內存的垃圾回收

垃圾回收的主要內存區域在堆內存中,先來看一下堆內存的內存結構:
堆內部結構劃分

1. Survivor區詳解

接着上面的GC來說,From區中對象一開始只有Eden區和From區中有對象,To區是空的

此時,進行一次GC操作,From區中對象的年齡就會加1,我們知道Eden區中所有存活的對象會被複制到To區,From區中對象有兩個去處——如果對象的年齡達到之前設置好的年齡閾值,此處的對象會被移動到Old區中,沒有達到閾值的對象會被複制到To;這時候From和To交換角色,之前的From變成了To,之前的To變成了From,也就是說無論如何都要保證名稱To的Survivor區域是空的;Minor Gc會一直重複重複這個過程,直到To區被填滿,然後會將所有對象複製到老年代中

2.Old區

存儲年齡比較大的對象或相對超過了某個閾值的對象;在Old區也會有GC的操作,Old區的GC通常被稱爲Major GC,每次GC之後還存活的對象年齡也會加1,如果年齡超過了某個閾值,就會被回收

我是一個普通的Java對象,我出生在Eden區,在Eden區我還看到和我長的很像的小兄弟,我們在Eden區中玩了挺長時間。 有一天Eden區中的人實在是太多了,我就被迫去了Survivor區的“From”區,自從去了Survivor區,我就開始漂了,有時 候在Survivor的“From”區,有時候在Survivor的“To”區,居無定所。直到我18歲的時候,爸爸說我成人了,該去社會上 闖闖了。 於是我就去了年老代那邊,年老代裏,人很多,並且年齡都挺大的,我在這裏也認識了很多人。在年老代裏,我生活了20年 (每次GC加一歲),然後被回收。

3. 內存回收策略

  1. 對象優先在Eden分配

  2. 大對象直接進入老年代

    # 當對象內存大於設置值時,直接進入老年代
    -XX:PretenureSizeThreshold
    
  3. 長期存活的對象將進入老年代
    如果對象在Eden區中經過第一次Minor GC後任然存活,並且被Survior容納的話,將被移動到Survior空間,並且對象年齡設置爲1,對象在Survior區中每熬過一次Monior GC,年齡增加1歲,當它的年齡增加到一定程度,就將會被晉升到老年代

    # 設置對象晉升到老年代的年齡
    -XX:MaxTenuringThreshold  
    
  4. 動態對象年齡判斷
    如果Survivor空間中相同年齡所有對象的大小的總和大於Survivor空間的一半,年齡大於或者等於該年齡的對象就可以直接進入老年代;

  5. 空間分配擔保
    在發生Minor GC 之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有的對象總空間,如果這個條件成立,那麼Minor GC 可以確保是安全的;如果不成立,則虛擬機會查看HeadlePromotionFailure設置值是否允許擔保失敗;如果允許,那麼會繼續檢查老年代最大最大可用連續空間是否大於歷次晉升到老年代對象的平均大小,如果大於,將嘗試進行一次Minor GC,儘管這次Minor GC是有風險的;如果小於,或者HandlePromotionFailure 設置不允許冒險,那麼這時要改爲進行一次Full GC

4. 回收過程圖示

在這裏插入圖片描述

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