1. 程式人生 > >JVM三種垃圾收集演算法思想及發展過程

JVM三種垃圾收集演算法思想及發展過程

JVM垃圾收集演算法的具體實現有很多種,本文只是介紹實現這些垃圾收集演算法的三種思想和發展過程。所有的垃圾收集演算法的具體實現都是遵循這三種演算法思想而實現的。

1.標記-清除演算法

標記-清除(Mark-Sweep)演算法是最基礎的垃圾收集演算法。正如其名字描述的那樣,該演算法分為兩個階段:“標記”和“清除”。首先標記出所有可以被回收的物件,然後經過一輪垃圾回收將所有被標記的物件的記憶體空間釋放,即清除可被回收的物件。標記-清除演算法的執行過程如下圖所示:

mark-sweep

該演算法的優點是邏輯簡單,最初想到的垃圾收集演算法就是這種。之後的收集演算法都是針對該演算法的不足進行改進而產生的新演算法。該演算法主要存在兩種不足:一是標記和清除兩個階段都比較低效;二是這種演算法雖然收回了記憶體,但是容易導致記憶體中有大量的不連續的小的記憶體區域(記憶體碎片)。當有一個大的物件需要建立時,可能出現雖然總的可用記憶體很多,但是沒有一個連續的大的記憶體空間來存放大物件的情況,從而導致了過早的再一次GC。

2.複製演算法

為了解決標記-清除演算法效率低以及記憶體碎片的問題,出現了複製(copying)演算法。該演算法的思想是將整個記憶體空間分為兩個大小相同的空間。同一時期只在一個記憶體空間上建立物件,當該部分記憶體滿了之後,執行垃圾回收。垃圾回收的過程:首先將不能回收的依然可用的物件複製到另一個記憶體空間上,然後將之前的一個記憶體空間直接清空。經過該輪垃圾回收後,之前的一個記憶體空間物件被全部清空了,活著的物件被複制到了另一個記憶體空間上,此時再建立物件就在另一個記憶體空間上分配空間了。複製演算法的執行過程如下圖所示:

copying

該演算法的優點是邏輯簡單,執行高效。但缺點是實際只使用了一半的可用記憶體,浪費了一半的記憶體。在商業的JVM中一般使用複製演算法來回收新生代的記憶體空間。因為新生代中的物件大部分都是使用一次就被拋棄的物件,所以新生代中的物件的存活率是很低的。絕大多數情況下符合80:20的原則。複製演算法的具體實現上,是將新生代記憶體區域劃分為8:1:1的三塊區域,8的區域叫做Eden區,兩個1都叫做Survivor區,分配記憶體時,首先在一個Survivor區和Eden區分配,當垃圾回收時,將上述兩個區的活著的物件複製到另一個Survivor區,然後將Eden區和原來的那個Survivor區清空。這樣的話使用了整個可用記憶體的90%,大大減小了浪費。當然,會出現垃圾回收時一個Survivor區裝不下所有存活物件的情況(很少發生),當出現時,就會進行分配擔保,由其他記憶體區域(一般是老年代)來存放溢位的物件。

3.標記-整理演算法

複製演算法對於物件存活率很低的情況是高效的,但是當物件的存活率非常高時,就變得非常低效了。在老年代中,物件的存活率很高,所以不能使用複製演算法。於是根據老年代的物件特點,提出了標記-整理(Mark-Compact)演算法。標記-整理演算法也分為兩個階段:標記和整理。第一個階段與標記-清除演算法一樣:標記出所有可以被回收的物件。第二個階段不再是簡單的清除無用物件的空間,而是將後面的活著的物件依次向前移動。將所有的活著的物件都移動成記憶體空間中前段連續一個區域,之後的連續的區域都是可分配的沒有使用的記憶體空間。標記-整理演算法的執行過程如下圖所示:

mark-compact

4.小結:分代收集思想

當前商業虛擬機器中一般採用“分代收集演算法”。分代收集演算法是根據物件的特點將記憶體空間分成不同的區域(即不同的代),對每個區域使用合適的收集演算法。在JVM中一般分為新生代和老年代,新生代中物件的存活率比較低,使用複製演算法簡單高效;在老年代中,由於物件的存活率較高,所以一般採用標記-整理演算法。
對於哪種演算法最好,我只能說,沒有最好的演算法,只有最適合某一種場景的某一種演算法。正是因為垃圾收集的物件各有特點,才誕生了多種不同的收集演算法。就像語言一樣,沒有必要去爭論Java最好,C++最強。每一個語言的存在必然有其使用場景和適合的地方。兼聽則明,多多學習就是了。