JVM 垃圾收集器與記憶體分配策略

ZMXQQ233發表於2020-11-11

JVM 垃圾收集器與記憶體分配策略

JVM記憶體區域可知Java執行時記憶體的各個區域。其中程式計數器、虛擬機器棧、本地方法棧3個區域隨執行緒而生,隨執行緒而滅,當方法結束或者執行緒結束時,記憶體就會跟著被回收了。

而只有處於執行期間,我們才能知道程式究竟會建立哪些物件,建立多少個物件,所以Java堆方法區這兩個區域記憶體的分配和回收時動態的,垃圾收集器也只關注這部分記憶體的管理。


一.物件存活判斷

1.引用計數演算法

在物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加一;當引用失效時,計數器值就減一;任何時候計數器值為零的物件就是不可能再被使用的。

  • 優點:實現簡單,效率高
  • 缺點:無法解決物件相互迴圈引用的問題——會導致物件的引用雖然存在,但是已經不可能再被使用,卻無法被回收。

2.可達性分析演算法

通過一系列的稱為”GC Roots”的物件作為起始點, 從這些節點開始向下搜尋, 搜尋走過的路徑稱為引用鏈(Reference Chain), 當一個物件到GC Roots不可達(也就是不存在引用鏈)的時候, 證明物件是不可用的。如下圖: Object5、6、7 雖然互有關聯, 但它們到GC Roots是不可達的, 因此也會被判定為可回收的物件。

image.png

在Java, 可作為GC Roots的物件包括:

  • 在虛擬機器棧中引用的物件
  • 在方法區中類靜態屬性引用的物件
  • 在方法區中常量引用的物件
  • 在本地方法棧中JNI(Native方法)引用的物件
  • Java虛擬機器內部的引用
  • 所有被同步鎖(synchronized關鍵字)持有的物件

二.如何回收(垃圾收集演算法)

分代收集理論:

  1. 弱分代假說:絕大多數物件都是朝生夕滅的。
  2. 強分代假說:熬過多次垃圾收集的物件就越難以消亡。

Java堆劃分為新生代和老生代兩個區域。在新生代中,每次垃圾收集時都發現有大批物件死去,而每次回收後存活的少量物件,將會逐步晉升到老年代中存放。

1.標記-清除演算法

分為標記和清除兩個階段先標記出需要回收的物件(可達性分析演算法或者引用計數演算法),在標記完成後統一回收所有被標記的物件。

缺點:

  • 效率問題,標記和清除效率都不高。
  • 空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾回收動作。
    image.png

2.標記-複製演算法

將可用記憶體劃分為大小相等的兩塊,每次只使用其中的一塊。當這塊用完了,就將還存活的複製到另一塊上,然後將已使用過的另一半記憶體一次性清除。

新生代中的物件98%都是朝生夕死的。將記憶體分為較大Eden和兩個較小的survivor空間。每次使用Eden和其中一個survivor,回收時將存活的物件一次性地複製到另一塊survivor中,再清理掉Eden和已用過的survivor。

HotSpot虛擬機器Eden與Survivor預設的大小比例為8:1:1。即只讓10%的新生代被浪費的,survivor空間不夠時,需要依賴其他記憶體(老年代)進行分配擔保,即讓物件進入老年代。

優點:

  • 對整個半區進行回收,不會出現空間碎片。
  • 如果記憶體中多數物件都是可回收,就只需複製少數的存活物件。

缺點:

  • 如果記憶體中多數物件都是存活的,將產生大量的記憶體間複製的開銷。
  • 可用記憶體縮減了一半,造成空間浪費。

image.png

3.標記-整理演算法

複製在物件存活率較高時效率很低。根據老年代的特點提出該演算法。標記過程同標記清除一樣,但不是直接對可回收物件進行清理,而是讓存活物件朝著一端移動,然後直接清理掉邊界以外的記憶體。

標記-整理與標記-清除的差異在於整理是移動式的回收演算法,清除是非移動的。

image.png

根據各年代特點分別採用最適當的GC演算法。

在新生代中每次垃圾收集都能發現大批物件已死, 只有少量存活,因此選用標記-複製演算法, 只需要複製少量存活物件就可以完成收集。

在老年代因為物件存活率高、沒有額外空間對它進行分配擔保, 就必須採用**“標記—清理標記—整理”**演算法來進行回收, 不必進行記憶體複製, 且直接騰出空閒記憶體。即:

  • 新生代:存活率低,使用複製演算法
  • 老年代:存活率高,使用“標記-整理”或“標記-清除”演算法

三.垃圾收集器

  • 新生代:Serial收集器  ParNew收集器  Parallel Scavenge收集器
  • 老年代:Serial Old收集器  Parallel Old收集器  CMS收集器

image.png

新生代:

1.Serial收集器

標記複製。單執行緒收集器,只會使用一個處理器或一條收集執行緒進行垃圾收集,而且在垃圾收集時,必須暫停其他所有工作執行緒,直到收集結束。優點:簡單高效。

image.png

2.ParNew收集器

標記複製。是Serial收集器的多執行緒並行版本,同時使用多條執行緒進行垃圾收集,其餘與Serial一樣。目前唯一能與CMS收集器配合工作。

image.png

3.Parallel Scavenge收集器

標記複製。並行收集的多執行緒收集器。CMS等收集器的關注點是儘可能地縮短垃圾收集時使用者執行緒的停頓時間,而PS收集器的目的則是達到一個可控制的吞吐量。吞吐量即CPU用於執行使用者程式碼的時間與CPU總消耗時間的比值(吞吐量=執行使用者程式碼的時間/(執行使用者程式碼的時間+垃圾收集的時間))。

image.png

老年代:

4.Serial Old收集器

標記整理。Serial 收集器的老年代版本,單執行緒收集器。

5.Parallel Old收集器

標記整理。Parallel Old是Parallel Scavenge的老年代版本,多執行緒收集器。

6.CMS收集器(Concurrent Mark Sweep併發標記清除)

標記清除。

併發收集,回收停頓時間短。

步驟:

  1. 初始標記:停掉使用者其他執行緒,僅標記GCRoots能直接關聯到的物件,速度很快。
  2. 併發標記:從GCRoots的直接關聯物件開始遍歷整個物件圖的過程,耗時長但不需要停頓使用者執行緒,與垃圾收集執行緒一起併發執行。
  3. 重新標記:修正併發標記期間因使用者程式繼續運作而導致標記產生變動的那一部分物件的標記記錄。耗時比初始標記長一點,但遠低於併發標記。
  4. 併發清除:清理刪除掉那些標記階段判斷為死亡的物件,因為標記清楚演算法不需要移動存活物件,所以這個階段也是可以與使用者執行緒同時併發的。

總體而言,CMS收集器的記憶體回收過程是與使用者執行緒一起併發執行的。

image.png

缺點:

  • CMS對處理器資源非常敏感。
  • CMS無法處理浮動垃圾(Floating Garbage),可能出現Concurrent Mode Failure失敗而導致另一次Full GC的產生。
  • CMS是標記清除,會產生大量碎片空間,對大物件記憶體分配帶來麻煩。

7.G1收集器(Garbage First)

與其他收集器不同,G1把連續的Java堆劃分為多個大小相等的獨立區域(Region),每一個Region都可以根據需要扮演新勝達的Eden空間、Survivor空間,或者老年代空間。收集器對不同角色的Region採用不同的策略去處理。優先處理回收價值收益最大的那些Region,也就是Garbage First的由來。

  • 從整體來看:“標記-整理” 演算法
  • 從區域性(兩個Region之間)來看:“複製”演算法

四.記憶體分配與回收策略

堆記憶體劃分為 Eden、Survivor 和 Tenured/Old 空間,如下圖所示:

image.png

從年輕代空間(包括 Eden 和 Survivor 區域)回收記憶體被稱為 Minor GC,對老年代GC稱為Major GC,而Full GC是對整個堆來說的。

  • 新生代GC(Minor GC):發生在新生代的垃圾收集動作,非常頻繁,一般回收速度也比較快。
  • 老年代GC(Major GC/Full GC):發生在老年代的垃圾收集動作,一般會伴隨Minor GC 速度一般比Minor GC慢上10倍以上。

物件的記憶體分配從大方向講,就是在堆上分配,物件主要分配在新生代的Eden區上,如果啟動了本地執行緒分配緩衝,將按執行緒優先在TLAB上分配,少數情況下也有可能直接分配在老年代中,分配的規則並非百分之百固定的,細節取決於當前使用的是哪一種垃圾收集器的組合,還有虛擬機器中與記憶體相關的引數設定。

1.物件優先分配在Eden

大多數情況下,物件在新生代的Eden區分配,當Eden區沒有足夠空間時,虛擬機器將發起一次Minor GC。

2.大物件直接進入老年代

所謂大物件是指需要大量連續記憶體空間的物件,最典型的大物件就是那種很長的字串以及陣列。大物件對虛擬機器的記憶體分配來說就是一個壞訊息,經常出現大物件容易導致記憶體還有不少空間時就提前觸發GC以獲取足夠的連續空間來“安置”它們。

3.長期存活的物件將進入老年代

虛擬機器給每個物件定義了一個物件年齡(Age)計數器。如果物件在Eden出生並經過第一次Minor GC後仍然存活,並且能夠被Survivor容納,將被移動到Survivor空間中,並且物件年齡為1.物件在Survivor區每“熬過”一次Minor GC,年齡就增加一歲,當年齡增加到一定程度(預設為15歲),就將被晉升到老年代中。物件晉升老年代年齡的閾值,可以通過引數設定。

4.動態物件年齡判定

如果在Survivor空間中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或者等於該年齡的物件就可以直接進入老年代,無需等到要求的年齡

5.空間分配擔保

發生Minor GC之前,虛擬機器會檢查老年代最大可用的連續空間是否大於新生代所有物件的總空間,如果成立,那麼Minor GC確保是安全的,如果不成立,則虛擬機器會檢視HandlePromotionFailure設定值是否允許擔保失敗。如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大於歷次晉升到老年代物件的平均大小,如果大於,將嘗試進行一次Minor GC,儘管是有風險的,如果小於或者設定不允許冒險,那這時也要進行一次Full GC。

參考
Full GC

相關文章