JVM垃圾回收機制

橡皮筋兒發表於2021-11-11

一、回收堆區

垃圾回收器在堆進行垃圾回收前,首先要判斷這些物件那些還存活,那些已經“死去”。判斷物件是否已“死”有如下幾種演算法:

1.引用計數法

給物件增加一個引用計數器,每當有一個地方引用它時,計數器就+1;

當引用失效時,計數器就-1;

任何時刻計數器為0的物件就是不能再被使用的,即物件已“死”。

引用計數法實現簡單,判定效率也比較高,在大部分情況下都是一個比較好的演算法。

但是,在主流的JVM中沒有選用引用計數法來管理記憶體,最主要的原因是引用計數法無法解決物件的迴圈引用問題。

2. 可達性分析演算法

在上面講了,Java並不採用引用計數法來判斷物件是否已“死”,而採用“可達性分析”來判斷物件是否存活。

通過一系列稱為“GC Roots”的物件作為起始點,從這些節點開始向下搜尋,搜尋走過的路徑稱為“引用鏈”,當一個物件到 GC Roots 沒有任何的引用鏈相連時(從 GC Roots 到這個物件不可達)時,證明此物件不可用。以下圖為例:

在Java語言中,可作為GC Roots的物件包含以下幾種:

  1. 虛擬機器棧(棧幀中的本地變數表)中引用的物件。
  2. 方法區中靜態屬性引用的物件
  3. 方法區中常量引用的物件
  4. 本地方法棧中(Native方法)引用的物件

引用

JDK1.2以前,Java中引用的定義很傳統: 如果引用型別的資料中儲存的數值代表的是另一塊記憶體的起始地址,就稱這塊記憶體代表著一個引用。

這種定義有些狹隘,一個物件在這種定義下只有被引用或者沒有被引用兩種狀態。

JDK1.2之後,Java對引用的概念做了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)四種,這四種引用的強度依次遞減。

1.強引用

類似於"Object obj = new Object()"這類的引用,只要強引用還存在,垃圾回收器永遠不會回收掉被引用的物件例項。

當記憶體空 間不足,Java虛擬機器寧願丟擲OutOfMemoryError錯誤,使程式異常終止,也不會靠隨意回收具有強引用的物件來解決記憶體不足問題。

2.軟引用

如果一個物件只具有軟引用,那就類似於可有可無的生活用品。

如果記憶體空間足夠,垃圾回收器就不會回收它,如果記憶體空間不足了,就會回收這些物件的記憶體。

軟引用可用來實現記憶體敏感的快取記憶體。

舉例:檢視網頁,可能後退,那麼剛才的網頁要不要一直儲存,一直儲存就是強引用,不儲存就是回收,那麼折中一下,於是產生了弱引用,當記憶體空間足夠時,就不回收,不足時,再回收。這個根據記憶體敏感程度而變化而決定是否快取,就是記憶體敏感的快取記憶體。

3.弱引用

物件擁有更短暫的生命週期。

在gc執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了只具有弱引用的物件,不管當前記憶體空間是否充足,都會回收它的記憶體。

由於gc是一個優先順序很低的執行緒,因此不一定會很快發現那些只具有弱引用的物件。

4.虛引用

就是形同虛設,與其他幾種引用都不同,虛引用並不會決定物件的生命週期。

如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。

作用: 虛引用主要用來跟蹤物件被垃圾回收的活動

區別: 虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用佇列 (ReferenceQueue)聯合使用。

當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列是否已經加入了虛引用,來了解被引用的物件是否將要被垃圾回收。

如果程式發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取必要的行動。

3.真正判決

要宣告一個物件的真正死亡,至少要經歷兩次標記過程

如果物件在進行可達性分析之後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選。

篩選的條件是此物件是否有必要執行finalize()方法。

​ 沒必要:沒有覆蓋finalize()方法finalize()方法已經被呼叫過一次了

審判

如果這個物件被判定為有必要執行finalize()方法,那麼這個物件將會被放置在一個叫做F-Queue的佇列之中,並在稍後由一個虛擬機器自動建立的、低優先順序的Finalizer執行緒去執行它

如果物件在finalize()中成功拯救自己:與引用鏈上的任何一個物件建立起關聯關係

那在第二次標記時它將會被移除出"即將回收"的集合,也就暫時逃脫死亡的命運了。

如果物件這時候還是沒有逃脫,那基本上它就是真的被回收了。

二、回收方法區

方法區(永久代)的垃圾回收主要收集兩部分內容:廢棄常量無用類。

廢棄常量:

沒有任何一個String物件引用常量池中的"abc"常量,也沒有其他地方引用這個字面量,如果此時發生GC並且有必要的話,這個"abc"常量會被系統清理出常量池。

常量池中的其他類(介面)、方法、欄位的符號引用也與此類似。

無用類

1.該類的所有例項都已經被回收(即在Java堆中不存在任何該類的例項)
2.載入該類的ClassLoader已被回收
3.該類對應的Class物件沒有任何其他地方被引用,無法在任何地方通過反射訪問該類的方法

三、垃圾回收演算法

1.標記-清除演算法

首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件

“標記-清除”演算法的不足主要有兩個

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

2.複製演算法(新生代回收演算法)

它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中一塊。

當這塊記憶體需要進行垃圾回收時,會將此區域還存活著的物件複製到另一塊上面,然後再把已經使用過的記憶體區域一次清理掉。

因為複製過去後,另一邊的記憶體肯定是連續的了,此時再把使用過得記憶體區域清理,從而達到了整理的效果。

也就是伊甸園區移動到survive0和survive1區的演算法。

但是,伊甸園區的物件都是朝生夕死的,所以並不需要1:1的空間,所以出現了8:1:1的預設比例

3.標記整理演算法(老年代回收演算法)

複製收集演算法在物件存活率較高時會進行比較多的複製操作,效率會變低。因此在老年代一般不能使用複製演算法。

而是採用標記整理演算法

標記過程仍與“標記-清除”過程一致,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活物件向一端移動,然後直接清理掉除存活物件以外的記憶體。流程圖如下:

4.分代收集演算法

就是將堆區分開,不同的位置採用不同的演算法

新生代中,每次垃圾回收都有大批物件死去,只有少量存活,因此我們採用複製演算法;

老年代中物件存活率高,就必須採用"標記-清理"或者"標記-整理"演算法。

四 .Minor GC、Major GC、Full GC的區別?

Minor GC 又稱為新生代GC 指的是發生在新生代的垃圾回收操作(包括Eden區和Survivor區)。

當年輕代記憶體空間被用完時,就會觸發垃圾回收。這個垃圾回收叫做Minor GC。

Major GC通常是跟full GC是等價的,收集整個GC堆。

但因為HotSpot VM發展了這麼多年,外界對各種名詞的解讀已經完全混亂了

Full GC定義是相對明確的,就是針對整個新生代、老生代、元空間(metaspace,java8以上版本取代perm gen)的全域性範圍的GC。

針對HotSpot VM GC來看

它裡面的GC其實準確分類只有兩大種:

Partial GC:並不收集整個GC堆的模式

  • Young GC:只收集年輕代的GC
  • Old GC:只收集老年代的GC。只有CMS的concurrent collection是這個模式
  • Mixed GC:收集整個年輕代以及老年代的GC。只有G1有這個模式

Full GC:收集整個堆,包括young gen、old gen、perm gen(如果存在的話)等所有部分的模式。

相關文章