天天看點

垃圾回收機制-2 垃圾收集算法1.标記 - 清除算法2.複制算法3.标記 - 整理算法4.分代收集算法5.GC方式6.安全點與安全區域

     垃圾收集算法包含四種算法:标記-清除算法、複制算法、标記-整理算法、分代收集算法

1.标記 - 清除算法

     标記 - 清除算法分為“标記”和“清除”兩個階段:首先标記出所有需要回收的對象,在标記完成後統一回收所有被标記的對象。

     标記:标記的過程其實就是,周遊所有的GC Roots,然後将所有GC Roots可達的對象标記為存活的對象。

     清除:清除的過程将周遊堆中所有的對象,将沒有标記的對象全部清除掉。

     執行過程:

垃圾回收機制-2 垃圾收集算法1.标記 - 清除算法2.複制算法3.标記 - 整理算法4.分代收集算法5.GC方式6.安全點與安全區域

标記 - 清除算法主要有兩個不足:

1.一個是效率問題,标記和清除兩個過程的效率都不高;

2.另一個是空間問題,标記清除後悔産生大量不連續的記憶體碎片,空間碎片太多可能會導緻以後在程式運作過程中需要配置設定較大對象時,無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

2.複制算法

       複制算法将可用記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着的對象複制到另一塊上面,然後在把已使用過得記憶體空間一次清理掉。這樣使得每次都是對整個半區進行記憶體回收,記憶體配置設定時也就不用考慮記憶體碎片等複雜情況,隻要移動堆頂指針,按順序配置設定記憶體即可,實作簡單,運作高效。

執行過程:

垃圾回收機制-2 垃圾收集算法1.标記 - 清除算法2.複制算法3.标記 - 整理算法4.分代收集算法5.GC方式6.安全點與安全區域

複制算法有一個不足:

将記憶體縮小為原來的一半,使用率太低。

       該算法一般用于新生代的垃圾回收,新生代中對象98%都是“朝生夕死”,是以不需要安裝1:1配置設定,而是将記憶體分為一塊較大的Eden區和兩塊較小From Survivor區和 To Survivor區,每次使用Eden區和其中的一塊 Survivor區。當回收時,将Eden區和Survivor中還存活着的對象一次性複制到另外一塊Survivor區中,最後清理掉Eden和剛才用過的Survivor區。當Survivor區空間不夠用時,需要依賴其他記憶體(老年代)進行配置設定擔保。

3.标記 - 整理算法

       複制算法在對象存活率較高時就要進行較多的複制操作,效率将會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行配置設定擔保,以對應被使用的記憶體中所有對象都100%存活的極端情況,是以老年代一般不能直接選用這種算法。

       标記 - 整理算法,标記過程仍然與“标記 - 清除”算法一樣,但後續步驟不是直接對可回收對象進行清理,而是讓所有存活對象都向一端移動,然後直接清理掉端邊界以外的記憶體。

執行過程:

垃圾回收機制-2 垃圾收集算法1.标記 - 清除算法2.複制算法3.标記 - 整理算法4.分代收集算法5.GC方式6.安全點與安全區域

4.分代收集算法

       現在商業虛拟機都采用分代收集算法,根據對象存活周期的不同将記憶體劃分為幾塊,一般是新生代和老年代,根據各個年代的特點采用最适合的收集算法,在新生代中,每次垃圾收集時都發現有大批對象死去,隻有少量存活,那就選用複制算法,隻需要付出少量存活對象的複制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行配置設定擔保,就必須使用“标記 - 整理”或者“标記 - 清除”算法來進行回收。

垃圾回收機制-2 垃圾收集算法1.标記 - 清除算法2.複制算法3.标記 - 整理算法4.分代收集算法5.GC方式6.安全點與安全區域

5.GC方式

5.1保守GC

       保守式GC(conservative GC):對堆棧資料進行遞歸掃描

       在進行GC的時候,JVM開始從一些已知位置(例如說JVM棧)開始掃描記憶體,掃描的時候每看到一個數字就看看它“像不像是一個指向GC堆中的指針”。這裡會涉及上下邊界檢查(GC堆的上下界是已知的)、對齊檢查(通常配置設定空間的時候會有對齊要求,假如說是4位元組對齊,那麼不能被4整除的數字就肯定不是指針),之類的。然後遞歸的這麼掃描出去。

       保守式GC的好處是相對來說實作簡單些,而且可以友善的用在對GC沒有特别支援的程式設計語言裡提供自動記憶體管理功能。Boehm-Demers-Weiser GC是保守式GC中的典型代表,可以嵌入到C或C++等語言寫的程式中

保守式GC的缺點有: 

1、會有部分對象本來應該已經死了,但有疑似指針指向它們,使它們逃過GC的收集。這對程式語義來說是安全的,因為所有應該活着的對象都會是活的;但對記憶體占用量來說就不是件好事,總會有一些已經不需要的資料還占用着GC堆空間。具體實作可以通過一些調節來讓這種無用對象的比例少一些,可以緩解(但不能根治)記憶體占用量大的問題。

2、由于不知道疑似指針是否真的是指針,是以它們的值都不能改寫;移動對象就意味着要修正指針。換言之,對象就不可移動了。有一種辦法可以在使用保守式GC的同時支援對象的移動,那就是增加一個間接層,不直接通過指針來實作引用,而是添加一層“句柄”(handle)在中間,所有引用先指到一個句柄表裡,再從句柄表找到實際對象。這樣,要移動對象的話,隻要修改句柄表裡的内容即可。但是這樣的話引用的通路速度就降低了。Sun JDK的Classic VM用過這種全handle的設計,但效果實在算不上好。

5.2半保守式GC

        半保守式GC:掃描棧的時候仍然會跟上面說的過程一樣,但掃描到GC堆内的對象時因為對象帶有足夠類型資訊了,JVM就能夠判斷出在該對象内什麼位置的資料是引用類型了。單獨配置設定一塊記憶體記錄資料

       JVM可以選擇在棧上不記錄類型資訊,而在對象上記錄類型資訊。這樣的話,掃描棧的時候仍然會跟上面說的過程一樣,但掃描到GC堆内的對象時因為對象帶有足夠類型資訊了,JVM就能夠判斷出在該對象内什麼位置的資料是引用類型了。

       為了支援半保守式GC,運作時需要在對象上帶有足夠的中繼資料。如果是JVM的話,這些資料可能在類加載器或者對象模型的子產品裡計算得到,但不需要JIT編譯器的特别支援。

5.3準确式GC

      将對象的引用位址都存入OopMap中,這樣虛拟機就直接可以得知哪些地方存放着對象引用。

      OopMap:OopMap 記錄了棧上本地變量到堆上對象的引用關系;

6.安全點與安全區域

       垃圾收集時,收集線程會對棧上的記憶體進行掃描,看看哪些位置存儲了 Reference 類型。如果發現某個位置确實存的是 Reference 類型,就意味着它所引用的對象這一次不能被回收。但問題是,棧上的本地變量表裡面隻有一部分資料是 Reference 類型的(它們是我們所需要的),那些非 Reference 類型的資料對我們而言毫無用處,但我們還是不得不對整個棧全部掃描一遍,這是對時間和資源的一種浪費。 

       一個很自然的想法是,能不能用空間換時間,在某個時候把棧上代表引用的位置全部記錄下來,這樣到真正 gc 的時候就可以直接讀取,而不用再一點一點的掃描了。事實上,大部分主流的虛拟機也正是這麼做的,比如 HotSpot ,它使用一種叫做 OopMap 的資料結構來記錄這類資訊。 

       我們知道,一個線程意味着一個棧,一個棧由多個棧幀組成,一個棧幀對應着一個方法,一個方法裡面可能有多個安全點。 gc 發生時,程式首先運作到最近的一個安全點停下來,然後更新自己的 OopMap ,記下棧上哪些位置代表着引用。枚舉根節點時,遞歸周遊每個棧幀的 OopMap ,通過棧中記錄的被引用對象的記憶體位址,即可找到這些對象( GC Roots )。

       Savepoint為了讓GC發生時停止所有的線程(不包括執行JNI調用線程)都“跑”到最近的安全點上再停頓下來。一般采用搶先式中斷和主動式中斷。

       搶先式中斷:在GC發生時,首先把所有線程全部中斷,如果發現有線程中斷的地方不在安全點上,就恢複線程,讓它“跑”到安全點上。不足:當線程中有sleep()、wait()時,此線程将一直或者需要長時間的等待。

      主動式中斷:當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單的設定一個标志,各個線程執行時主動去輪詢這個标志,發現中斷标志為真時就自己中斷挂起。

6.1安全區域

       使用Safepoint似乎已經完美地解決了如何進入GC的問題,但實際情況卻并不一定。Safepoint機制保證了程式執行時,在不太長的時間内就會遇到可進入GC的Safepoint。但是,程式“不執行”的時候呢?所謂的程式不執行就是沒有配置設定CPU時間,典型的例子就是線程處于Sleep狀态或者Blocked狀态,這時候線程無法響應JVM的中斷請求,“走”到安全的地方去中斷挂起,JVM也顯然不太可能等待線程重新被配置設定CPU時間。對于這種情況,就需要安全區域(Safe Region)來解決。

       安全區域是指在一段代碼片段之中,引用關系不會發生變化。在這個區域中的任意地方開始GC都是安全的。我們也可以把Safe Region看做是被擴充了的Safepoint。線上程執行到Safe Region中的代碼時,首先辨別自己已經進入了Safe Region,那樣,當在這段時間裡JVM要發起GC時,就不用管辨別自己為Safe Region狀态的線程了。線上程要離開Safe Region時,它要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,那線程就繼續執行,否則它就必須等待直到收到可以安全離開Safe Region的信号為止。