JVM—垃圾回收與演算法

JVM—垃圾回收與演算法

前言

今天就來介紹一下垃圾回收與演算法。

垃圾回收與演算法

JVM—垃圾回收與演算法

如何確定垃圾

引用計數法

在 Java 中,引用和物件是有關聯的。如果要操作物件則必須用引用進行。因此,很顯然一個簡單的辦法是透過引用計數來判斷一個物件是否可以回收。簡單說,即一個物件如果沒有任何與之關聯的引用,即他們的引用計數都不為0,則說明物件不太可能再被用到,那麼這個物件就是可回收物件。

可達性分析

為了解決引用計數法的迴圈引用問題,Java 使用了可達性分析的方法。透過一系列的“GC roots” 物件作為起點搜尋。如果在“GC roots”和一個物件之間沒有可達路徑,則稱該物件是不可達的。要注意的是,不可達物件不等價於可回收物件,不可達物件變為可回收物件至少要經過兩次標記過程。兩次標記後仍然是可回收物件,則將面臨回收。

標記清除演算法(Mark-Sweep)

最基礎的垃圾回收演算法,分為兩個階段,標註和清除。標記階段標記出所有需要回收的物件,清 除階段回收被標記的物件所佔用的空間。如圖

JVM—垃圾回收與演算法

從圖中我們就可以發現,該演算法最大的問題是記憶體碎片化嚴重,後續可能發生大物件不能找到可 利用空間的問題。

複製演算法(copying)

為了解決 Mark-Sweep 演算法記憶體碎片化的缺陷而被提出的演算法。按記憶體容量將記憶體劃分為等大小的兩塊。每次只使用其中一塊,當這一塊記憶體滿後將尚存活的物件複製到另一塊上去,把已使用的記憶體清掉,如圖:

JVM—垃圾回收與演算法

這種演算法雖然實現簡單,記憶體效率高,不易產生碎片,但是最大的問題是可用記憶體被壓縮到了原 本的一半。且存活物件增多的話,Copying 演算法的效率會大大降低。

標記整理演算法(Mark-Compact)

結合了以上兩個演算法,為了避免缺陷而提出。標記階段和 Mark-Sweep 演算法相同,標記後不是清理物件,而是將存活物件移向記憶體的一端。然後清除端邊界外的物件。如圖:

分代收集演算法

分代收集法是目前大部分 JVM 所採用的方法,其核心思想是根據物件存活的不同生命週期將記憶體劃分為不同的域,一般情況下將 GC 堆劃分為老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特點是每次垃圾回收時只有少量物件需要被回收,新生代的特點是每次垃圾回收時都有大量垃圾需要被回收,因此可以根據不同區域選擇不同的演算法。

新生代與複製演算法

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

JVM—垃圾回收與演算法

老年代與標記複製演算法

而老年代因為每次只回收少量物件,因而採用 Mark-Compact 演算法。

JAVA 虛擬機器提到過的處於方法區的永生代(Permanet Generation),它用來儲存 class 類, 常量,方法描述等。 對永生代的回收主要包括廢棄常量和無用的類。

物件的記憶體分配主要在新生代的 Eden Space 和 Survivor Space 的 From Space(Survivor 目前存放物件的那一塊),少數情況會直接分配到老生代。

當新生代的 Eden Space 和 From Space 空間不足時就會發生一次 GC,進行 GC 後,Eden Space 和 From Space 區的存活物件會被挪到 To Space,然後將 Eden Space 和 From Space 進行清理。

如果 To Space 無法足夠儲存某個物件,則將這個物件儲存到老生代。

在進行 GC 後,使用的便是 Eden Space 和 To Space 了,如此反覆迴圈。

當物件在 Survivor 區躲過一次 GC 後,其年齡就會+1。預設情況下年齡到達 15 的物件會被移到老生代中。

GC 分代收集演算法 VS 分割槽收集演算法

分代收集演算法

當前主流 VM 垃圾收集都採用”分代收集”(Generational Collection)演算法, 這種演算法會根據 物件存活週期的不同將記憶體劃分為幾塊, 如 JVM 中的 新生代、老年代、永久代,這樣就可以根據 各年代特點分別採用最適當的 GC 演算法

在新生代-複製演算法

每次垃圾收集都能發現大批物件已死, 只有少量存活。因此選用複製演算法, 只需要付出少量存活物件的複製成本就 可以完成收集。

在老年代-標記整理演算法

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

分割槽收集演算法

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

感謝諸君的觀看,文中如有紕漏,歡迎在評論區來交流。如果這篇文章幫助到了你,歡迎點贊關注。