《深入理解java虛擬機器》學習筆記3——垃圾回收演算法

yangxi_001發表於2013-12-04

Java虛擬機器的記憶體區域中,程式計數器、虛擬機器棧和本地方法棧三個區域是執行緒私有的,隨執行緒生而生,隨執行緒滅而滅;棧中的棧幀隨著方法的進入和退出而進行入棧和出棧操作,每個棧幀中分配多少記憶體基本上是在類結構確定下來時就已知的,因此這三個區域的記憶體分配和回收都具有確定性。垃圾回收重點關注的是堆和方法區部分的記憶體。

常用的垃圾回收演算法有:

(1).引用計數演算法:

給物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加1;當引用失效時,計數器值就減1;任何時刻計數器都為0的物件就是不再被使用的,垃圾收集器將回收該物件使用的記憶體。

引用計數演算法實現簡單,效率很高,微軟的COM技術、ActionScript、Python等都使用了引用計數演算法進行記憶體管理,但是引用計數演算法對於物件之間相互迴圈引用問題難以解決,因此java並沒有使用引用計數演算法。

(2).根搜尋演算法:

通過一系列的名為“GC Root”的物件作為起點,從這些節點向下搜尋,搜尋所走過的路徑稱為引用鏈(Reference Chain),當一個物件到GC Root沒有任何引用鏈相連時,則該物件不可達,該物件是不可使用的,垃圾收集器將回收其所佔的記憶體。

主流的商用程式語言C#、java和Lisp都使用根搜素演算法進行記憶體管理。

在java語言中,可作為GC Root的物件包括以下幾種物件:

a. java虛擬機器棧(棧幀中的本地變數表)中的引用的物件。

b.方法區中的類靜態屬性引用的物件。

c.方法區中的常量引用的物件。

d.本地方法棧中JNI本地方法的引用物件。

java方法區在Sun HotSpot虛擬機器中被稱為永久代,很多人認為該部分的記憶體是不用回收的,java虛擬機器規範也沒有對該部分記憶體的垃圾收集做規定,但是方法區中的廢棄常量和無用的類還是需要回收以保證永久代不會發生記憶體溢位。

判斷廢棄常量的方法:如果常量池中的某個常量沒有被任何引用所引用,則該常量是廢棄常量。

判斷無用的類:

(1).該類的所有例項都已經被回收,即java堆中不存在該類的例項物件。

(2).載入該類的類載入器已經被回收。

(3).該類所對應的java.lang.Class物件沒有任何地方被引用,無法在任何地方通過反射機制訪問該類的方法。

Java中常用的垃圾收集演算法:

(1).標記-清除演算法:

最基礎的垃圾收集演算法,演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成之後統一回收掉所有被標記的物件。

標記-清除演算法的缺點有兩個:首先,效率問題,標記和清除效率都不高。其次,標記清除之後會產生大量的不連續的記憶體碎片,空間碎片太多會導致當程式需要為較大物件分配記憶體時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

(2).複製演算法:

將可用記憶體按容量分成大小相等的兩塊,每次只使用其中一塊,當這塊記憶體使用完了,就將還存活的物件複製到另一塊記憶體上去,然後把使用過的記憶體空間一次清理掉。這樣使得每次都是對其中一塊記憶體進行回收,記憶體分配時不用考慮記憶體碎片等複雜情況,只需要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。

複製演算法的缺點顯而易見,可使用的記憶體降為原來一半。

(3).標記-整理演算法:

標記-整理演算法在標記-清除演算法基礎上做了改進,標記階段是相同的標記出所有需要回收的物件,在標記完成之後不是直接對可回收物件進行清理,而是讓所有存活的物件都向一端移動,在移動過程中清理掉可回收的物件,這個過程叫做整理。

標記-整理演算法相比標記-清除演算法的優點是記憶體被整理以後不會產生大量不連續記憶體碎片問題。

複製演算法在物件存活率高的情況下就要執行較多的複製操作,效率將會變低,而在物件存活率高的情況下使用標記-整理演算法效率會大大提高。

(4).分代收集演算法:

根據記憶體中物件的存活週期不同,將記憶體劃分為幾塊,java的虛擬機器中一般把記憶體劃分為新生代和年老代,當新建立物件時一般在新生代中分配記憶體空間,當新生代垃圾收集器回收幾次之後仍然存活的物件會被移動到年老代記憶體中,當大物件在新生代中無法找到足夠的連續記憶體時也直接在年老代中建立。

現在的Java虛擬機器就聯合使用了分代複製、標記-清除和標記-整理演算法,java虛擬機器垃圾收集器關注的記憶體結構如下:

堆記憶體被分成新生代和年老代兩個部分,整個堆記憶體使用分代複製垃圾收集演算法。

(1).新生代:

新生代使用複製和標記-清除垃圾收集演算法,研究表明,新生代中98%的物件是朝生夕死的短生命週期物件,所以不需要將新生代劃分為容量大小相等的兩部分記憶體,而是將新生代分為Eden區,Survivor from和Survivor to三部分,其佔新生代記憶體容量預設比例分別為8:1:1,其中Survivor from和Survivor to總有一個區域是空白,只有Eden和其中一個Survivor總共90%的新生代容量用於為新建立的物件分配記憶體,只有10%的Survivor記憶體浪費,當新生代記憶體空間不足需要進行垃圾回收時,仍然存活的物件被複制到空白的Survivor記憶體區域中,Eden和非空白的Survivor進行標記-清理回收,兩個Survivor區域是輪換的。

新生代中98%情況下空白Survivor都可以存放垃圾回收時仍然存活的物件,2%的極端情況下,如果空白Survivor空間無法存放下仍然存活的物件時,使用記憶體分配擔保機制,直接將新生代依然存活的物件複製到年老代記憶體中,同時對於建立大物件時,如果新生代中無足夠的連續記憶體時,也直接在年老代中分配記憶體空間。

Java虛擬機器對新生代的垃圾回收稱為Minor GC,次數比較頻繁,每次回收時間也比較短。

使用java虛擬機器-Xmn引數可以指定新生代記憶體大小。

(2).年老代:

年老代中的物件一般都是長生命週期物件,物件的存活率比較高,因此在年老代中使用標記-整理垃圾回收演算法。

Java虛擬機器對年老代的垃圾回收稱為MajorGC/Full GC,次數相對比較少,每次回收的時間也比較長。

當新生代中無足夠空間為物件建立分配記憶體,年老代中記憶體回收也無法回收到足夠的記憶體空間,並且新生代和年老代空間無法在擴充套件時,堆就會產生OutOfMemoryError異常。

java虛擬機器-Xms引數可以指定最小記憶體大小,-Xmx引數可以指定最大記憶體大小,這兩個引數分別減去Xmn引數指定的新生代記憶體大小,可以計算出年老代最小和最大記憶體容量。

(3).永久代:

java虛擬機器記憶體中的方法區在Sun HotSpot虛擬機器中被稱為永久代,是被各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯後的程式碼等資料。永久代垃圾回收比較少,效率也比較低,但是也必須進行垃圾回收,否則會永久代記憶體不夠用時仍然會丟擲OutOfMemoryError異常。

永久代也使用標記-整理演算法進行垃圾回收,java虛擬機器引數-XX:PermSize和-XX:MaxPermSize可以設定永久代的初始大小和最大容量。

相關文章