1. 程式人生 > 實用技巧 >Java垃圾回收演算法

Java垃圾回收演算法

垃圾回收

判斷物件是否是垃圾

引用計數法

在物件中新增一個引用計數器,如果被引用計數器加1,引用失效計數器減1,如果計數器為0則被標記為垃圾。

原理簡單,效率高。但是在Java中很少使用,因為存在物件間迴圈引用的問題,導致計數器無法清零。

可達性分析

主流語言的記憶體管理都使用可達性分析判斷物件是否存活。基本思路是通過一系列稱為“GC Roots”的根物件作為起始節點集,從這些節點開始,根據引用關係向下搜尋,搜尋過程走過的路徑稱為引用鏈,如果某個物件到GC Roots沒有任何引用鏈相連,則會標記為垃圾。

可作為GC Roots的物件:虛擬機器棧和本地方法棧中引用的物件、類靜態屬性引用的物件、常量引用的物件。

GC演算法

標記-清除演算法

分為標記和清除階段,首先從每個GC Roots出發依次標記有引用的物件,最後清除沒有標記的物件。

執行效率不穩定,如果堆包含大量物件且大部分需要回收,必須進行大量的標記清除,導致效率隨物件數量增長而降低。

存在記憶體空間碎片化問題,會產生大量不連續的記憶體碎片,導致以後需要分配大物件時容易出發FullGC。

在這裡插入圖片描述

標記-複製演算法

為了解決記憶體碎片化問題,將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中一塊。當使用的這塊空間用完了,就將存活物件複製到另一塊,再把已使用過的記憶體空間一次清理掉。主要用於新生代。

實現簡單,執行高效,解決了記憶體碎片化問題。代價是可用記憶體縮小為原來的一半,浪費空間。

HotSpot把新生代劃分為一塊較大的Eden和兩塊較小的Survivor(form,to),每次分配記憶體只使用Eden和Survivor中的一塊。垃圾收集時將Eden和Survivor(from)中仍然存活的物件一次性複製到另一塊Survivor(to)上,然後直接清理掉Eden和已經用過的Survivor(from)。from 和 to交換,to變成下一次的from,並且年齡+1,當年齡變成15時,移到老年代。

在這裡插入圖片描述

標記-整理演算法

標記整理演算法在物件存活率高時要進行較多的複製操作,效率低。如果不想浪費空間,就需要有額外空間分配擔保。應對被使用記憶體中所有物件都存活的極端情況,所以老年代一般不使用此演算法。

老年代使用標記-整理演算法,標記過程與標記-清除演算法一樣,但不直接清理可回收物件,而是讓所有存活物件都向記憶體空間一端移動,然後然後清理掉邊界以外的記憶體。

標記-清除與標記-整理的差異在於前者是一種非移動式演算法而後者是移動式。如果移動存活物件,尤其是在老年代這種每次回收都有大量物件存活的區域,是一種極為負重的操作。而且移動必須全稱暫停使用者執行緒。如果不移動物件就會導致空間碎片化問題,只能依賴更復雜的記憶體分配器和訪問器解決。

在這裡插入圖片描述

分代收集演算法

分代收集演算法是目前大部分JVM所採用的方法,其核心思想是根據物件存活的不同生命週期將記憶體劃分為不同的域,一般情況下將GC堆分為老年代和新生代。

老年代的特點是每次垃圾回收時只有少量物件需要被回收;

新生代的特點是每次垃圾回收時都有大量垃圾需要回收。因此不同區域有不同的演算法。

新生代–複製演算法

目前大部分JVM的GC對新生代都採取複製演算法,因為新生代的特點是每次垃圾回收時都有大量垃圾需要回收,即要複製的操作少,但通常不是1:1來劃分。一般將新生代劃分為一塊較大的Eden空間和兩個較小的Survivor空間(From Space,To Space),每次使用Eden空間和其中一塊Survivor空間,當進行回收時,將兩塊空間中還存活的物件複製到另一塊Survivor中。

老年代與標記整理演算法

老年代每次回收只需要少量的物件,因而採用標記整理演算法。

  • JAVA虛擬機器提到過的處於方法區的永久代,它用來儲存class類,常量,方法描述等。對永久代的回收主要包括廢棄常量和無用的類。
  • 物件的記憶體分配主要在新生代的Eden和Survivor的From Sapce(Survivor目前存放物件的那一塊),少數情況會直接分配到老年代。
  • 當新生代的Eden和From Space空間不足時就會發生一次GC,進行GC後,Eden和From Space區的存活物件會被挪到To Space,然後將Eden和From Space進行清理。
  • 如果To Space不足夠儲存某個物件,則將這個物件儲存到老年代。
  • 在進行GC 後,使用的便是Eden和To Space了,如此反覆。
  • 當物件在Survivor區躲過一次GC 後,其年齡 +1 ,預設情況下年齡到15的物件就會被移到老年代。

分割槽收集法

分割槽演算法是將整個堆空間劃分為連續的不同小區間,獨立回收。這樣做的好處是可以控制一次回收多少個小區間,根據目標停頓時間,每次合理地回收若干個小區間,從而減少一次GC所產生的停頓。

JAVA中的四種引用

強引用

Java中最常見的就是強引用。把物件賦給一個引用變數,這個變數就是一個強引用。當一個物件被強引用變數引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的,即使該物件以後永遠都不會被用到,JVM也不會回收。因此強引用是造成Java記憶體洩漏的主要原因。

軟引用

軟引用需要用Soft Reference類來實現,描述非必需物件。在系統將發生內粗你溢位前,會把軟引用關聯的物件加入回收範圍以獲得更多記憶體空間。對於只有軟引用的物件來說,當系統記憶體足夠時他不會被回收,當系統記憶體空間不足時它會被回收。軟引用通常用在對記憶體敏感的程式中。用來快取伺服器中間計算結果及不需要實時儲存的使用者行為等。

弱引用

弱引用需要用WeakReference類來實現,描述非必需物件,它比軟引用的生存週期更短,對於只有弱引用的物件來說,只要垃圾回收機制一執行,不管JVM的記憶體空間是否足夠,總會回收該物件佔用的記憶體。

虛引用

虛引用需要PhantomReference類來實現,它不能單獨使用,必須和引用佇列聯合使用。虛引用的作用主要是跟蹤物件被垃圾回收的狀態

GC垃圾回收器

Java堆記憶體被劃分為新生代和年老代兩部分,新生代主要使用複製和標記-清除演算法;

年老代主要使用標記-整理垃圾回收演算法,因此Java虛擬機器針對新生代和年老代分別提供了多種不同的垃圾收集器,jdk1.6中Sun HotSpot虛擬機器的垃圾收集器如下:

在這裡插入圖片描述

Serial(單執行緒、複製演算法)

最基礎的收集器,使用複製演算法、單執行緒工作,只用於一個處理器或一條執行緒完成垃圾回收,進行垃圾收集時必須暫停其他所有工作執行緒。

Serial是虛擬機器在客戶端模式的預設新生代收集器,簡單高效,對於記憶體受限的環境它是所有收集器中額外記憶體消耗最小的,對於處理器核心較少的環境,Serial由於沒有執行緒的互動開銷,可獲得最高的單執行緒收集效率。

PerNew(Serial+多執行緒)

Serial的多執行緒版本,除了使用多執行緒進行垃圾收集外其餘行為完全一致。

ParNew是虛擬機器在服務端模式的預設新生代收集器,一個重要原因是除了Serial外只有它能與CMS配合。自從JDK9開始,ParNew加CMS不再是官方推薦的解決方案,官方希望被G1取代。

在這裡插入圖片描述

Parallel Scavenge(多執行緒、複製演算法)

新生代收集器,基於複製演算法是可並行的多執行緒收集器,與ParNew類似。

特點是它的關注點與其他收集器不同,Parallel Scavenge的目標是達到一個可控制的吞吐量,吞吐量就是處理器用於執行使用者程式碼的時間與處理器消耗總時間的比值。

Serial Old(單執行緒、標記整理演算法)

Serial的老年代版本,單執行緒工作,使用標記-整理演算法。

Serial Old是虛擬機器在客戶端模式的預設老年代收集器,用於服務端有兩種用途:

  • 在JDK5及以前與Parallel Scavenge搭配
  • 作為CMS失敗預案

Serial和Serial Old搭配垃圾回收過程圖:

在這裡插入圖片描述

Parallel Old(多執行緒、標記整理演算法)

Parallel Scavenge的老年代版本,支援多執行緒,基於標記-整理演算法。JDK6提供,注重吞吐量可考慮Parallel Scavenge加Parallel Old。

在這裡插入圖片描述

CMS(多執行緒、標記清除演算法)

獲取最短回收停頓時間為目標,基於標記-清除演算法,過程相對複雜。

分為四個步驟:初始標記、併發標記、重新標記、併發清除。

初始標記和重新標記需要STW(Stop The World,系統停頓)

  • 初始標記僅是標記GC Roots能直接關聯的物件,速度很快。仍需要暫停所有工作執行緒。
  • 併發標記進行GC Roots跟蹤的過程,和使用者執行緒一起工作,不需要暫停工作執行緒.
  • 重新標記為了修正在併發標記期間,因使用者執行緒繼續執行而導致標記產生變動的那一部分的標記記錄,仍然需要暫停所有工作執行緒.
  • 併發清除清除GC Roots不可達物件,和使用者執行緒一起工作,不需要暫停工作執行緒,由於耗時最長併發標記和併發清除過程中,垃圾收集執行緒可以和使用者一起併發工作,所以整體上來看CMS收集器的記憶體回收和使用者執行緒是一起併發地執行。

CMS收集器工作過程:

在這裡插入圖片描述

缺點:

  • 對處理器資源敏感,併發階段雖然不會導致使用者執行緒暫停,但會降低吞吐量
  • 無法處理浮動垃圾,有可能出現併發失敗而導致Full GC
  • 基於標記-清除演算法,產生空間碎片。

G1(標記整理)

Garbage first垃圾收集器是目前垃圾收集器理論發展的最前沿成果,相比於CMS收集器,G1收集器兩個最突出的改進是:

  • 基於標記-整理演算法,不產生記憶體碎片
  • 可以非常精確控制停頓時間,在不犧牲吞吐量前提下,實現低停頓垃圾回收。

G1收集器避免全區域垃圾收集,它把堆記憶體劃分為大小固定的幾個獨立區域,並且跟蹤這些區域的垃圾收集進度,同時在後臺維護一個優先順序列表,每次根據所允許的收集時間,優先回收垃圾最多的區域。區域劃分和優先順序區域回收機制,確保G1收集器可以在有限時間獲得最高的垃圾收集效率。

G1的運作過程:

  • **初始標記:**標記GC Roots能直接關聯的物件,讓下一階段使用者執行緒併發執行時能正確地在可用Region中分配新物件。需要STW但耗時很短,在Minor GC時同步完成。
  • **併發標記:**從GC Roots開始對堆中物件進行可達性分析,遞迴掃描整個堆的物件圖。耗時長但可與使用者執行緒併發,掃描完成後要重新處理SATB記錄的在併發時有變動的物件。
  • **最終標記:**對使用者執行緒做短暫暫停,處理併發階段結束後仍然遺留下來的少量SATB記錄。
  • **篩選回收:**對各Region的回收價值排序,根據使用者期望停頓時間指定回收計劃。必須暫停使用者執行緒,由多條收集執行緒並行完成。

由使用者指定期望停頓時間是G1的一個強大功能,但該值不能設得太低,一般設定為100~300ms

在這裡插入圖片描述

ZGC

JDK11中加入的具有實驗性質的低延遲垃圾收集器,目標是儘可能在不影響吞吐量的前提下,實現在任意堆記憶體大小都可以把停頓時間限制在10ms以內的低延遲。

基於Region記憶體佈局,不設分代,使用了讀屏障、染色指標和記憶體多重對映等技術實現可併發的標記-整理,以低延遲為首要目標。

ZGC的Region具有動態性,是動態建立和銷燬的,並且容量大小也是動態變化的。

參考書籍:《深入理解Java虛擬機器》周志明

參考部落格:https://blog.csdn.net/u011080472/article/details/51324422

圖片來源於以上部落格(侵刪)

障、染色指標和記憶體多重對映等技術實現可併發的標記-整理,以低延遲為首要目標。

ZGC的Region具有動態性,是動態建立和銷燬的,並且容量大小也是動態變化的。

參考書籍:《深入理解Java虛擬機器》周志明

參考部落格:https://blog.csdn.net/u011080472/article/details/51324422

圖片來源於以上部落格(侵刪)