1. 程式人生 > 實用技巧 >Java虛擬機器詳解(三)------垃圾回收

Java虛擬機器詳解(三)------垃圾回收

  Java和C++之間有一堵由記憶體動態分佈和垃圾回收技術所圍成的高牆,牆外面的人想進去,牆裡面的人想出來。

1、為什麼要進行垃圾回收?

  Java是一門面向物件的語言,在一個系統執行中,會伴隨著很多物件的建立,而這些物件一旦建立了就佔據了一定的記憶體,建立的物件是儲存在堆中的,當物件使用完畢之後,不對其進行清理,那麼會一直佔據記憶體空間,很明顯記憶體空間是有限的,如果不回收這些無用的物件佔據的記憶體,那麼新建立的物件申請不了記憶體空間,系統就會丟擲異常而無法執行,所以必須要經常進行記憶體的回收,也就是垃圾收集。

2、那些區域記憶體需要回收?

    Java執行時的記憶體結構,其中程式計數器、虛擬機器棧、本地方法棧這三個區域是執行緒私有的,隨執行緒而生,隨執行緒而滅,棧中的棧幀隨著方法的進入和退出而有條不紊的執行著入棧和出棧操作,這幾個區域的記憶體分配和回收都具備確定性,在方法結束或執行緒結束時,記憶體也就跟著回收了,所以不需要我們考慮。

  那麼現在就剩下Java堆方法區了,這兩塊區域在編譯期間我們並不能完全確定建立多少個物件,有些是在執行時期建立的物件,所以Java記憶體回收機制主要是作用在這兩塊區域。

3、如何判斷物件為垃圾物件

  1)引用計數法

  這種演算法是這樣的:給每一個建立的物件增加一個引用計數器,每當有一個地方引用它時,這個計數器就加1;而當引用失效時,這個計數器就減1。當這個引用計數器值為0時,也就是說這個物件沒有任何地方在使用它了,那麼這就是一個無效的物件,便可以進行垃圾回收了。

  這種演算法實現簡單,而且效率也很高。但是Java沒有采用該演算法來進行垃圾回收,因為這種演算法無法解決物件之間的迴圈引用問題。

  2)可達性分析演算法

  我們這裡直接給出結論:在主流的商用程式中(Java,C#),都是使用根搜尋演算法(GC Roots Tracing)來判定物件是否存活。

  該演算法思路:通過一系列名為“GC Roots” 的物件作為終點,當一個物件到GC Roots 之間無法通過引用到達時,那麼該物件便可以進行回收了。

  如下圖:物件object5、object6、object7雖然互有關聯,但是他們到GC Roots是不可達的,所以他們將會被判定是可回收的物件。

  在Java語言中,有如下4中物件可以作為 GC Roots:

  PS:紅色的物件是要被當做垃圾回收的!

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

4、如何進行垃圾回收

  垃圾回收涉及到大量的程式細節,而且各個平臺的虛擬機器操作記憶體的方式也不一樣,但是他們進行垃圾回收的演算法是通用的,所以這裡我們也只介紹幾種通用演算法。

  ①、標記-清除演算法

  演算法實現:分為標記-清除兩個階段,首先根據上面的根搜尋演算法標記出所有需要回收的物件,在標記完成後,然後在統一回收掉所有被標記的物件。

  缺點

  1、效率低:標記和清除這兩個過程的效率都不高。

  2、容易產生記憶體碎片:因為記憶體的申請通常不是連續的,那麼清除一些物件後,那麼就會產生大量不連續的記憶體碎片,而碎片太多時,當有個大物件需要分配記憶體時,便會造成沒有足夠的連續記憶體分配而提前觸發垃圾回收,甚至直接丟擲OutOfMemoryExecption。

  ②、複製演算法

  為了解決標記-清除演算法的兩個缺點,複製演算法誕生了。

  演算法實現:將可用記憶體按容量劃分為大小相等的兩塊區域,每次只使用其中一塊,當這一塊的記憶體用完了,就將還活著的物件複製到另一塊區域上,然後再把已使用過的記憶體空間一次性清理掉。

  優點:每次都是隻對其中一塊記憶體進行回收,不用考慮記憶體碎片的問題,而且分配記憶體時,只需要移動堆頂指標,按順序進行分配即可,簡單高效。

  缺點:將記憶體分為兩塊,但是每次只能使用一塊,也就是說,機器的一半記憶體是閒置的,這資源浪費有點嚴重。並且如果物件存活率較高,每次都需要複製大量的物件,效率也會變得很低。

  ③、標記-整理演算法

  上面我們說過複製演算法會浪費一半的記憶體,並且物件存活率較高時,會有過多的複製操作,效率低下。

  如果物件存活率很高,基本上不會進行垃圾回收時,標記-整理演算法誕生了。

  演算法實現:首先標記出所有存活的物件,然後讓所有存活物件向一端進行移動,最後直接清理到端邊界以外的記憶體。

  侷限性:只有物件存活率很高的情況下,使用該演算法才會效率較高。

  ④、分代收集演算法

  當前商業虛擬機器都是採用此演算法,但是其實這不是什麼新的演算法,而是上面幾種演算法的合集。

  演算法實現:根據物件的存活週期不同將記憶體分為幾塊,然後不同的區域採用不同的回收演算法。

    1、對於存活週期較短,每次都有大批物件死亡,只有少量存活的區域,採用複製演算法,因為只需要付出少量存活物件的複製成本即可完成收集;

    2、對於存活週期較長,沒有額外空間進行分配擔保的區域,採用標記-整理演算法,或者標記-清除演算法。

  比如,對於 HotSpot 虛擬機器,它將堆空間分為如下兩塊區域:

5、何時進行垃圾回收

理清了什麼是垃圾,怎麼回收垃圾,最後一點就是Java虛擬機器何時進行垃圾回收呢?

  程式設計師可以呼叫 System.gc()方法,手動回收,但是呼叫此方法表示希望進行一次垃圾回收。但是它不能保證垃圾回收一定會進行,而且具體什麼時候進行是取決於具體的虛擬機器的,不同的虛擬機器有不同的對策。

  其次虛擬機器會自行根據當前記憶體大小,判斷何時進行垃圾回收,比如前面所說的,新生代滿了,新產生的物件無法分配記憶體時,便會觸發垃圾回收機制。

  這裡需要說明的是宣告一個物件死亡,至少要經歷兩次標記,前面我們說過,如果物件與GC Roots 不可達,那麼此物件會被第一次標記並進行一次篩選,篩選的條件是此物件是否有必要執行 finalize() 方法,當物件沒有覆蓋 finalize()方法,或者該方法已經執行了一次,那麼虛擬機器都將視為沒有必要執行finalize()方法。

  如果這個物件有必要執行 finalize() 方法,那麼該物件將會被放置在一個有虛擬機器自動建立、低優先順序,名為 F-Queue 佇列中,GC會對F-Queue進行第二次標記,如果物件在finalize() 方法中成功拯救了自己(比如重新與GC Roots建立連線),那麼第二次標記時,就會將該物件移除即將回收的集合,否則就會被回收。