深入JVM:(三)垃圾回收演算法
一、哪些記憶體需要回收
引用計數演算法
給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器為0的物件就是不可能再被使用的
引用計數演算法(Reference Counting)
的實現簡單,判定效率也很高,在大部分情況下它都是一個不錯的演算法。但是Java虛擬機器裡面沒有選用引用計數演算法來管理記憶體,其中最主要的原因是它很難解決物件之間相互迴圈引用的問題。
可達性分析演算法
通過一系列的稱為"GC Roots"的物件作為起始點,從這些節點開始向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Roots沒有任何引用鏈相連時,則證明此物件是不可用的。

可達性分析.png
Java對引用的概念進行了擴充,將引用分為 強引用(Strong Reference
)、 軟引用(Soft Reference
)、 弱引用(Weak Reference)
、 虛引用(Phantom Reference)
4種,這4種引用強度依次逐漸減弱。
強引用
就是指在程式程式碼之中普遍存在的,類似"Object obj=new Object()"這類的引用,只要強引用還存在,垃圾收集器永遠不會回收掉被引用的物件。
軟引用
是用來描述一些還有用但並非必需的物件。對於軟引用關聯著的物件,在系統將要發生記憶體溢位異常之前,將會把這些物件列進回收範圍之中進行第二次回收。如果這次回收還沒有足夠的記憶體,才會丟擲記憶體溢位異常。在JDK 1.2之後,提供了SoftReference類來實現軟引用。
弱引用
也是用來描述非必需物件的,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生之前。當垃圾收集器工作時,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。在JDK 1.2之後,提供了WeakReference類來實現弱引用。
虛引用
也稱為幽靈引用或者幻影引用,它是最弱的一種引用關係。一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。在JDK 1.2之後,提供了PhantomReference類來實現虛引用。
二、什麼時候回收
如果物件在進行可達性分析後發現沒有與GC Roots相連線的引用鏈,那它將會被第一次標記並且進行一次篩選,篩選的條件是此物件是否有必要執行 finalize()
方法。當物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,虛擬機器將這兩種情況都視為“沒有必要執行”。
如果這個物件被判定為有必要執行 finalize()
方法,那麼這個物件將會放置在一個叫做 F-Queue
的佇列之中,並在稍後由一個由虛擬機器自動建立的、低優先順序的 Finalizer
執行緒去執行它。這裡所謂的“執行”是指虛擬機器會觸發這個方法,但 並不承諾會等待它執行結束
,這樣做的原因是,如果一個物件在finalize()方法中執行緩慢,或者發生了死迴圈(更極端的情況),將很可能會導致F-Queue佇列中其他物件永久處於等待,甚至導致整個記憶體回收系統崩潰。finalize()方法是物件逃脫死亡命運的最後一次機會,稍後GC將對F-Queue中的物件進行第二次小規模的標記,如果物件要在finalize()中成功拯救自己——只要重新與引用鏈上的任何一個物件建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變數或者物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合;如果物件這時候還沒有逃脫,那基本上它就真的被回收了。
三、如何回收
回收方法區
永久代的垃圾收集主要回收兩部分內容:廢棄常量和無用的類。回收廢棄常量與回收Java堆中的物件非常類似。以常量池中字面量的回收為例,假如一個字串"abc"已經進入了常量池中,但是當前系統沒有任何一個String物件是叫做"abc"的,換句話說,就是沒有任何String物件引用常量池中的"abc"常量,也沒有其他地方引用了這個字面量,如果這時發生記憶體回收,而且必要的話,這個"abc"常量就會被系統清理出常量池。常量池中的其他類(介面)、方法、欄位的符號引用也與此類似。
判定一個常量是否是“廢棄常量”比較簡單,而要判定一個類是否是“無用的類”的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是“無用的類”:
- 該類所有的例項都已經被回收,也就是Java堆中不存在該類的任何例項。
- 載入該類的ClassLoader已經被回收。
- 該類對應的java.lang.Class物件沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
標記-清除演算法
首先標記出所有需要回收的物件,在標記完成後統一回收所有被標記的物件

標記-清除演算法.png
它的主要不足有兩個:
- 一個是效率問題,標記和清除兩個過程的效率都不高;
- 另一個是空間問題,標記清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致以後在程式執行過程中需要分配較大物件時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作
複製演算法
將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。

複製演算法.png
這樣使得每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。只是這種演算法的代價是將記憶體縮小為了原來的一半,未免太高了一點。
標記-整理演算法
首先標記出所有需要回收的物件,讓所有存活的物件都向一端移動,然後直接清理掉端邊界以外的記憶體

標記-整理演算法.png
分代收集演算法
把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點採用最適當的收集演算法。在新生代中,每次垃圾收集時都發現有大批物件死去,只有少量存活,那就選用複製演算法,只需要付出少量存活物件的複製成本就可以完成收集。而老年代中因為物件存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記—清理”或者“標記—整理”演算法來進行回收