JVM之垃圾回收(1-概述+演算法)

大象大象你的鼻子怎麼那麼長發表於2020-09-29

1.概述

什麼是垃圾

Java = (C++)–

什麼是垃圾( Garbage) 呢?  
➢垃圾是指在執行程式中沒有任何指標指向的物件,這個物件就是需要被回收的垃圾。  
➢外文: An object is considered garbage when it can no longer be reached from any pointer in the runningprogram. 如果不及時對記憶體中的垃圾進行清理,那麼,這些垃圾物件所佔的記憶體空間會一直保留到應用程式結束,被保留的空間無法被其他物件使用。甚至可能導致記憶體溢位。

  • 垃圾收集,不是Java語言的伴生產物。早在1960年,第一門開始使用記憶體動態分配和垃圾收集技術的Lisp語言誕生。

    • 關於垃圾收集有三個經典問題:

      • ➢哪些記憶體需要回收?
      • ➢什麼時候回收?
      • ➢如何回收?
  • 垃圾收集機制是Java的招牌能力,極大地提高了開發效率。如今,垃圾收集幾乎成為現代語言的標配,即使經過如此長時間的發展,Java的垃圾收集機制仍然在不斷的演進中,不同大小的裝置、不同特徵的應用場景,對垃圾收集提出了新的挑戰,這當然也是面試的熱點。

在這裡插入圖片描述

大廠面試題

螞蟻金服:

你知道哪幾種垃圾回收器,各自的優缺點,重點講一下 cms和g1

一面: JVM GC演算法有哪些,目前的JDK版本採用什麼回收演算法
一面: ( G1回收器講下回收過程

GC是什麼?為什麼要有GC?

一面: GC的兩種判定方法? CMS收集器與G1收集器的特點。

百度:

說一下GC演算法,分代回收說下
垃圾收集策略和演算法

天貓:

一面: jvm GC原理,JVM怎麼回收記憶體
一面: CMS特點,垃圾回收演算法有哪些?各自的優缺點,他們共同的缺點是什麼?

滴滴:

一面: java的垃圾回收器都有哪些,說下g1的應用場景,平時你是如何搭配使用垃圾回收器的

京東:

你知道哪幾種垃圾收集器,各自的優缺點,重點講下cms和G1,包括原理,流程,優缺點。垃圾回收演算法的實現原理。

阿里:

講一講垃圾回收演算法。
什麼情況下觸發垃圾回收?
如何選擇合適的垃圾收集演算法?
JVM有哪三種垃圾回收器?

位元組跳動:

常見的垃圾回收器演算法有哪些,各有什麼優劣?
system.gc ()和runtime.gc()會做什麼事情?
一面: Java GC機制? GC Roots有哪些?
二面: Java物件的回收方式,回收演算法。
CMS和G1瞭解麼,CMS解決什麼問題,說一下回收的過程。
CMS回收停頓了幾次,為什麼要停頓兩次。

為什麼需要GC

  • 對於高階語言來說,一個基本認知是如果不進行垃圾回收,記憶體遲早都會被消耗完,因為不斷地分配記憶體空間而不進行回收,就好像不停地生產生活垃圾而從來不打掃一樣。
  • 除了釋放沒用的物件,垃圾回收也可以清除記憶體裡的記錄碎片。碎片整理將所佔用的堆記憶體移到堆的一端,以便JVM 將整理出的記憶體分配給新的物件。
  • 隨著應用程式所應付的業務越來越龐大、複雜,使用者越來越多,沒有GC就不能保證應用程式的正常進行。而經常造成STW的GC又跟不上實際的需求,所以才會不斷地嘗試對GC進行優化。

早期垃圾回收

  • 在早期的C/C++時代,垃圾回收基本上是手工進行的。開發人員可以使用new關鍵字進行記憶體申請,並使用delete關鍵字進行記憶體釋放。比如以下程式碼:
MibBridge *pBridge = new cmBaseGroupBridge () ;
//如果註冊失敗,使用Delete釋放該物件所佔記憶體區域
if (pBridge->Register(kDestroy)!= NO_ERROR)
delete pBridge;

  • 這種方式可以靈活控制記憶體釋放的時間,但是會給開發人員帶來頻繁申請和釋放記憶體的管理負擔。倘若有一處記憶體區間由於程式設計師編碼的問題忘記被回收,那麼就會產生記憶體洩漏,垃圾物件永遠無法被清除,隨著系統執行時間的不斷增長,垃圾物件所耗記憶體可能持續上升,直到出現記憶體溢位並造成應用程式崩潰。
  • 在有了垃圾回收機制後,上述程式碼塊極有可能變成這樣:
MibBridge *pBridge = new cmBaseGroupBridge();
pBridge 一> Register(kDestroy);

  • 現在,除了Java以外,C#、Python、 Ruby等語言都使用了自動垃圾回收的思想,也是未來發展趨勢。可以說,這種自動化的記憶體分配和垃圾回收的方式己經成為現代開發語言必備的標準。

Java垃圾回收機制

  • 自動記憶體管理,無需開發人員手動參與記憶體的分配與回收,這樣降低記憶體洩漏和記憶體溢位的風險

    • 沒有垃圾回收器,java也會和cpp一樣,各種懸垂指標,野指標,洩露問題讓你頭疼不已。
  • 自動記憶體管理機制,將程式設計師從繁重的記憶體管理中釋放出來,可以更專心地專注於業務開發

  • oracle官閘道器於垃圾回收的介紹

    • https://docs.oracle.com/javase/8/docs/ technotes/guides/vm/gctuning/toc.html
  • 對於Java開發人員而言,自動記憶體管理就像是一個黑匣子,如果過度依賴於“自動”,那麼這將會是一場災難,最嚴重的就會弱化Java開發人員在程式出現記憶體溢位時定位問題和解決問題的能力。

  • 此時,了 解JVM的自動記憶體分配和記憶體回收原理就顯得非常重要,只有在真正瞭解JVM是如何管理記憶體後,我們才能夠在遇見OutOfMemoryError時, 快速地根據錯誤異常日誌定位問題和解決問題。

  • 當需要排查各種記憶體溢位、記憶體洩漏問題時,當垃圾收整合為系統達到更高併發量的瓶頸時,我們就必須對這些“自動化”的技術實施必要的監控和調節。

  • 垃圾回收器可以對年輕代回收,也可以對老年代回收,甚至是全堆和方法區的回收。

    • 其中,Java堆是垃圾收集器的工作重點。
  • 從次數上講:

    • 頻繁收集Young區
    • 較少收集01d區
    • 基本不動Perm區

在這裡插入圖片描述

2. 垃圾回收相關演算法

垃圾標記階段:物件存活判斷

  • 在堆裡存放著幾乎所有的Java物件例項,在GC執行垃圾回收之前,首先需要區分出記憶體中哪些是存活物件,哪些是已經死亡的物件。只有被標記為己經死亡的物件,GC才會在執行垃圾回收時,釋放掉其所佔用的記憶體空間,因此這個過程我們可以稱為垃圾標記階段。
  • 那麼在JVM中究竟是如何標記一個死亡物件呢?簡單來說,當一個物件已經不再被任何的存活物件繼續引用時,就可以宣判為已經死亡。
  • 判斷物件存活一般有兩種方式:引用計數演算法可達性分析演算法

2.1 標記階段:法1_引用計數法 (java沒有采用)

  • 引用計數演算法(Reference Counting)比較簡單,對每個物件儲存一個整型 的引用計數器屬性。用於記錄物件被引用的情況。

  • 對於一個物件A,只要有任何一個物件引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要物件A的引用計數器的值為0,即表示物件A不可能再被使用,可進行回收。

  • 優點:實現簡單,垃圾物件便於辨識;判定效率高,回收沒有延遲性。

  • 缺點:

    • ➢它需要單獨的欄位儲存計數器,這樣的做法增加了儲存空間的開銷。
    • ➢每次賦值都需要更新計數器,伴隨著加法和減法操作,這增加了時間開銷。
    • ➢引用計數器有一個嚴重的問題,即無法處理迴圈引用的情況。這是一 條致命缺陷,導致在Java的垃圾回收器中沒有使用這類演算法

在這裡插入圖片描述
圖示分析證明java沒有采用引用計數法
在這裡插入圖片描述
如果不下小心直接把0bj1 一reference和0bj2 一reference置null。 則在Java堆當中的兩塊記憶體依然保持著互相引用,無法回收。

/**
 * -XX:+PrintGCDetails
 * 證明:java使用的不是引用計數演算法
 */
public class RefCountGC {
    //這個成員屬性唯一的作用就是佔用一點記憶體
    private byte[] bigSize = new byte[5 * 1024 * 1024];//5MB

    Object reference = null;

    public static void main(String[] args) {
        RefCountGC obj1 = new RefCountGC();
        RefCountGC obj2 = new RefCountGC();

        obj1.reference = obj2;
        obj2.reference = obj1;

        obj1 = null;
        obj2 = null;
        //顯式的執行垃圾回收行為
        //這裡發生GC,obj1和obj2能否被回收?
        System.gc();

        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

小結

  • 引用計數演算法, 是很多語言的資源回收選擇,例如因人工智慧而更加火熱的Python,它更是同時支援引用計數和垃圾收集機制。

  • 具體哪種最優是要看場景的,業界有大規模實踐中僅保留引用計數機制,以提高吞吐量的嘗試。

  • Java並沒有選擇引用計數,是因為其存在一個基本的難題,也就是很難處理迴圈引用關係。

  • Python 如何解決迴圈引用?

    • ➢手動解除: 很好理解,就是在合適的時機,解除引用關係。
    • ➢使用弱引用weakref,weakref是Python提供的標準庫,旨在解決迴圈引用。

2.2 標記階段:法2_可達性分析演算法

也叫根搜尋演算法或追蹤性垃圾收集

  • 相對於引用計數演算法而言,可達性分析演算法不僅同樣具備實現簡單和執行高
    效等特點,更重要的是該演算法可以有效地解決在引用計數演算法中迴圈引用的問題,防止記憶體洩漏的發生。

  • 相較於引用計數演算法,這裡的可達性分析就是Java、C#選擇的。這種型別的垃圾收集通常也叫作追蹤性垃圾收集(Tracing GarbageCollection)。

  • 所謂"GC Roots"根集合就是一組必須活躍的引用。

  • 基本思路:

    • ➢可達性分析演算法是以根物件集合(GCRoots)為起始點,按照從上至下的方式搜尋被根物件集合所連線的目標物件是否可達。
    • ➢使用可達性分析演算法後,記憶體中的存活物件都會被根物件集合直接或間接連線著,搜尋所走過的路徑稱為引用鏈(Reference Chain)
    • ➢如果目標物件沒有任何引用鏈相連,則是不可達的,就意味著該物件己經死亡,可以標記為垃圾物件。
    • ➢在可達性分析演算法中,只有能夠被根物件集合直接或者間接連線的物件才是存活物件。
      在這裡插入圖片描述
      GC Roots
      在Java語言中,GC Roots包括以下幾類元素:
  • 虛擬機器棧中引用的物件

    • ➢比如:各個執行緒被呼叫的方法中使用到的引數、區域性變數等。
  • 本地方法棧內JNI(通常說的本地方法)引用的物件

  • 方法區中類靜態屬性引用的物件

    • ➢比如:Java類的引用型別靜態變數
  • 方法區中常量引用的物件

    • ➢比如:字串常量池(string Table) 裡的引用
  • 所有被同步鎖synchroni zed持有的物件

  • Java虛擬機器內部的引用。

    • ➢基本資料型別對應的Class物件,一些常駐的異常物件(如:
  • NullPointerException、OutOfMemoryError) ,系統類載入器。

  • 反映java虛擬機器內部情況的JMXBean、JVMTI中註冊的回撥、原生程式碼快取等

  • 除了這些固定的GCRoots集合以外,根據使用者所選用的垃圾收集器以及當
    前回收的記憶體區域不同,還可以有其他物件“臨時性”地加入,共同構成完整GC Roots集合。比如:分代收集和區域性回收(Partial GC)。

    • ➢如果只針對Java堆中的某一塊區域進行垃圾回收(比如:典型的只針
      對新生代),必須考慮到記憶體區域是虛擬機器自己的實現細節,更不是孤立封閉的,這個區域的物件完全有可能被其他區域的物件所引用,這時候就需要一.並將關聯的區域物件也加入GC Roots集合中去考慮,才能保證可達性分析的準確性。
  • 小技巧:由於Root採用棧方式存放變數和指標,所以如果一個指標,它儲存了堆記憶體裡面的物件,但是自己又不存放在堆記憶體裡面,那它就是一個Root
    在這裡插入圖片描述
    注意

  • 如果要使用可達性分析演算法來判斷記憶體是否可回收,那麼分析工作必須在
    一個能保障一致性的快照中進行。這點不滿足的話分析結果的準確性就無法保證。

  • 這點也是導致GC進行時必須“StopTheWorld"的一個重要原因。

    • ➢即使是號稱(幾乎)不會發生停頓的CMS收集器中,列舉根節點時也是必須要停頓的。

2.3 物件的finalization機制

  • Java語言提供了物件終止(finalization)機制來允許開發人員提供物件被銷燬之前的自定義處理邏輯。

  • 當垃圾回收器發現沒有引用指向一個物件,即:垃圾回收此物件之前,總會先呼叫這個物件的finalize()方法。

  • finalize()方法允許在子類中被重寫,用於在物件被回收時進行資源釋放。通常在這個方法中進行一些資源釋放和清理的工作,比如關閉檔案、套接字和資料庫連線等。

  • 應該交給垃圾回收機制呼叫。理由包括下面三點:永遠不要主動呼叫某個物件的finalize ()方法

    • ➢在finalize() 時可能會導致物件復活。
    • ➢finalize()方法的執行時間是沒有保障的,它完全由Gc執行緒決定,極端情況下,若不發生GC,則finalize() 方法將沒有執行機會。
    • ➢一個糟糕的finalize ()會嚴重影響GC的效能。
  • 從功能上來說,finalize()方法與C++ 中的解構函式比較相似,但是Java採用的是基於垃圾回收器的自動記憶體管理機制,所以finalize()方法在本質,上不同於C++ 中的解構函式。

物件是否"死亡"

  • 由於finalize ()方法的存在,虛擬機器中的物件一般處於三種可能的狀態

  • 如果從所有的根節點都無法訪問到某個物件,說明物件己經不再使用了。一般來說,此物件需要被回收。但事實上,也並非是“非死不可”的,這時候它們暫時處於“緩刑”階段。一個無法觸及的物件有可能在某一個條件下“復活”自己,如果這樣,那麼對它的回收就是不合理的,為此,定義虛擬機器中的物件可能的三種狀態。如下:

    • 可觸及的:從根節點開始,可以到達這個物件。
    • 可復活的:物件的所有引用都被釋放,但是物件有可能在finalize()中復活。
    • 不可觸及的:物件的finalize()被呼叫,並且沒有復活,那麼就會進入不可觸及狀態。不可觸及的物件不可能被複活,因為finalize() 只會被呼叫一一次。
  • 以上3種狀態中,是由於finalize()方法的存在,進行的區分。只有在物件不可觸及時才可以被回收。

判定是否可以回收具體過程
判定一個物件objA是否可回收,至少要經歷兩次標記過程:

  • 如果物件objA到GC Roots沒有引用鏈,則進行第一 次標記。

  • 進行篩選,判斷此物件是否有必要執行finalize()方法

    • ①如果對 象objA沒有重寫finalize()方法,或者finalize ()方法已經被虛擬機器呼叫過,則虛擬機器視為“沒有必要執行”,objA被判定為不可觸及的。
    • ②如果物件objA重寫了finalize()方法,且還未執行過,那麼objA會被插入到F一Queue佇列中,由一個虛擬機器自動建立的、低優先順序的Finalizer執行緒觸發其finalize()方法執行。
    • ③finalize()方法是物件逃脫死亡的最後機會,稍後Gc會對F一Queue佇列中的物件進行第二次標記。如果objA在finalize()方法中與引用鏈上的任何一個物件建立了聯絡,那麼在第二次標記時,objA會被移出“即將回收”集合。之後,物件會再次出現沒有引用存在的情況。在這個情況下,finalize方法不會被再次呼叫,物件會直接變成不可觸及的狀態,也就是說,一個物件的finalize方法只會被呼叫一次。

2.4 清除階段:法1_標記-清除演算法

當成功區分出記憶體中存活物件和死亡物件後,GC接下來的任務就是執行垃圾回收,釋放掉無用物件所佔用的記憶體空間,以便有足夠的可用記憶體空間為新物件分配記憶體.
  目前在JVM中比較常見的三種垃圾收集演算法是標記一清除演算法( Mark一Sweep)、複製演算法(Copying)、標記一壓縮演算法(Mark一Compact)
背景:
標記一清除演算法(Mark一Sweep)是一種非常基礎和常見的垃圾收集演算法,該演算法被J . McCarthy等人在1960年提出並並應用於Lisp語言。

執行過程:
當堆中的有效記憶體空間(available memory) 被耗盡的時候,就會停止整個程式(也被稱為stop the world),然後進行兩項工作,第一項則是標記,第二項則是清除。

  • 標記: Collector從引用根節點開始遍歷,標記所有被引用的物件。一般是在物件的Header中記錄為可達物件
  • 清除: Collector對堆 記憶體從頭到尾進行線性的遍歷,如果發現某個物件在其Header中沒有標記為可達物件,則將其回收

在這裡插入圖片描述

缺點

➢效率不算高
➢在進行Gc的時候,需要停止整個應用程式,導致使用者體驗差
這種方式清理出來的空閒記憶體是不連續的,產生記憶體碎片。需要維護一個空閒列表

注意:何為清除?

這裡所謂的清除並不是真的置空,而是把需要清除的物件地址儲存在空閒
的地址列表裡。下次有新物件需要載入時,判斷垃圾的位置空間是否夠,如果夠,就存放。

2.5 清除階段:法2_複製演算法

背景:
為了解決標記一清除演算法在垃圾收集效率方面的缺陷,M.L.Minsky於1963年發表了著名的論文,“ 使用雙儲存區的Li sp語言垃圾收集器CALISP Garbage Collector Algorithm Using SerialSecondary Storage )”。M.L. Minsky在該論文中描述的演算法被人們稱為複製(Copying)演算法,它也被M. L.Minsky本人成功地引入到了Lisp語言的一個實現版本中。

核心思想:
將活著的記憶體空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在.使用的記憶體中的存活物件複製到未被使用的記憶體塊中,之後清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,最後完成垃圾回收。
堆中S0和S1使用的就是複製演算法
在這裡插入圖片描述
優點:

  • 沒有標記和清除過程,實現簡單,執行高效
  • 複製過去以後保證空間的連續性,不會出現“碎片”問題。

缺點:

  • 此演算法的缺點也是很明顯的,就是需要兩倍的記憶體空間。
  • 對於G1這種分拆成為大量region的GC,複製而不是移動,意味著GC需要維護region之間物件引用關係,不管是記憶體佔用或者時間開銷也不小。
    特別的
    如果系統中的垃圾物件很多,複製演算法不會很理想,複製演算法需要複製的存活物件數量並不會太大,或者說非常低才行。

應用場景:
在新生代,對常規應用的垃圾回收,一次通常可以回收708一 99的記憶體空間。回收價效比很高。所以現在的商業虛擬機器都是用這種收集演算法回收新生代。
在這裡插入圖片描述

2.6 清除階段:法3_標記-壓縮(整理,Mark-Compact)演算法

背景:
  複製演算法的高效性是建立在存活物件少、垃圾物件多的前提下的。這種情況在新生代經常發生,但是在老年代,更常見的情況是大部分物件都是存活物件。如果依然使用複製演算法,由於存活物件較多,複製的成本也將很高。因此,基於老年代垃圾回收的特性,需要使用其他的演算法。
  標記一清除演算法的確可以應用在老年代中,但是該演算法不僅執行效率低下,而且在執行完記憶體回收後還會產生記憶體碎片,所以JVM的設計者需要在此基礎之上進行改進。標記一壓縮(Mark一Compact) 演算法由此誕生
  1970年前後,G. L. Steele 、C. J. Chene和D.S. Wise 等研究者釋出標記一壓縮演算法。在許多現代的垃圾收集器中,人們都使用了標記一壓縮演算法或其改進版本。
  
執行過程:

  • 第一階段和標記一清除演算法一樣,從根節點開始標記所有被引用物件.

  • 第二階段將所有的存活物件壓縮到記憶體的一端,按順序排放。

  • 之後,清理邊界外所有的空間。

在這裡插入圖片描述

  • 標記一壓縮演算法的最終效果等同於標記一清除演算法執行完成後,再進行一次記憶體碎片整理,因此,也可以把它稱為標記一清除一壓縮(Mark一 Sweep一Compact)演算法。

  • 二者的本質差異在於標記一清除演算法是一種非移動式的回收演算法,標記一壓.縮是移動式的。是否移動回收後的存活物件是一項優缺點並存的風險決策。

  • 可以看到,標記的存活物件將會被整理,按照記憶體地址依次排列,而未被標記的記憶體會被清理掉。如此一來,當我們需要給新物件分配記憶體時,JVM只需要持有一個記憶體的起始地址即可,這比維護一個空閒列表顯然少了許多開銷。

指標碰撞(Bump the Pointer )
如果記憶體空間以規整和有序的方式分佈,即已用和未用的記憶體都各自一邊,彼此之間維繫著一個記錄下一次分配起始點的標記指標,當為新物件分配記憶體時,只需要通過修改指標的偏移量將新物件分配在第一個空閒記憶體位置上,這種分配方式就叫做指標碰撞(Bump the Pointer) 。

優點

  • 消除了標記一清除演算法當中,記憶體區域分散的缺點,我們需要給新物件分配記憶體時,JVM只 需要持有一個記憶體的起始地址即可。
  • 消除了複製演算法當中,記憶體減半的高額代價。

缺點

  • 從效率.上來說,標記一整理演算法要低於複製演算法。
  • 移動物件的同時,如果物件被其他物件引用,則還需要調整引用的地址。· 移動過程中,需要全程暫停使用者應用程式。即: STW

2.7 小結

  • 效率上來說,複製演算法是當之無愧的老大,但是卻浪費了太多記憶體。
  • 而為了儘量兼顧上面提到的三個指標,標記一整理演算法相對來說更平滑一些,但是效率.上不盡如人意,它比複製演算法多了一個標記的階段,比標記一清除多了一個整理記憶體的階段。
    在這裡插入圖片描述

2.8 分代收集演算法

難道就沒有一種最優的演算法麼?
沒有最好的演算法,只有更合適的演算法
  前面所有這些演算法中,並沒有一種演算法可以完全替代其他演算法,它們都具有自己獨特的優勢和特點。分代收集演算法應運而生。
  分代收集演算法,是基於這樣一個事實:不同的物件的生命週期是不一樣的。因此,不同生命週期的物件可以採取不同的收集方式,以便提高回收效率。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點使用不同的回收演算法,以提高垃圾回收的效率。
  在Java程式執行的過程中,會產生大量的物件,其中有些物件是與業務資訊相關,比如Http請求中的Session物件、執行緒、Socket連線, 這類物件跟業務直接掛鉤,因此生命週期比較長。但是還有一些物件,主要是程式執行過程中生成的臨時變數,這些物件生命週期會比較短,比如: String物件, 由於其不變類的特性,系統會產生大量的這些物件,有些物件甚至只用一次即可回收。
  目前幾乎所有的GC都是採用分代收集(Generational Collecting) 演算法執行垃圾回收的。
  在HotSpot中,基於分代的概念,GC所使用的記憶體回收演算法必須結合年輕代和老年代各自的特點。

  • 年輕代(Young Gen)

    • 年輕代特點:區域相對老年代較小,物件生命週期短、存活率低,回收頻繁。
    • 這種情況複製演算法的回收整理,速度是最快的。複製演算法的效率只和當前存活物件大小有關,因此很適用於年輕代的回收。而複製演算法記憶體利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解。·
  • 老年代(Tenured Gen)

    • 老年代特點:區域較大,物件生命週期長、存活率高,回收不及年輕代頻繁。

    • 這種情況存在大量存活率高的物件,複製演算法明顯變得不合適。一般是由標記一清除或者是標記一清除與標記一整理的混合實現。

      • ➢Mark階段的開銷與存活物件的數量成正比。
      • ➢Sweep階段的開銷與所管理區域的大小成正相關。
      • ➢Compact階 段的開銷與存活物件的資料成正比。

以HotSpot中的CMS回收器為例,CMS是基於Mark一 Sweep實現的,對於物件的回收效率很高。而對於碎片問題,CMS採用基於Mark一Compact演算法的Serial 0ld回收器作為補償措施:當記憶體回收不佳(碎片導致的Concurrent Mode
Failure時),將採用Serial 0ld執行Full GC以達到對老年代記憶體的整理。
  分代的思想被現有的虛擬機器廣泛使用。幾乎所有的垃圾回收器都區分新生代和老年代。

2.9 增量收集演算法、分割槽演算法

增量收集演算法
上述現有的演算法,在垃圾回收過程中,應用軟體將處於一種stop the World的狀態。在Stop the World狀態下,應用程式所有的執行緒都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程式會被掛起很久,將嚴重影響使用者體驗或者系統的穩定性。為了解決這個問題,即對實時垃圾收集演算法的研究直接導致了增量收集(Incremental Collecting) 演算法的誕生。
基本思想
  如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那麼就可以讓垃圾收集執行緒和應用程式執行緒交替執行。每次,垃圾收集執行緒只收集一小片區域的記憶體空間,接著切換到應用程式執行緒。依次反覆,直到垃圾收集完成。
  總的來說,增量收集演算法的基礎仍是傳統的標記一清除和複製演算法。增量收集演算法通過對執行緒間衝突的妥善處理,允許垃圾收集執行緒以分階段的方式完成標記、清理或複製工作。
缺點:
使用這種方式,由於在垃圾回收過程中,間斷性地還執行了應用程式程式碼,所以能減少系統的停頓時間。但是,因為執行緒切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
分割槽演算法
  一般來說,在相同條件下,堆空間越大,一次GC時所需要的時間就越長,有關GC產生的停頓也越長。為了更好地控制GC產生的停頓時間,將一塊 大的記憶體區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減少一次GC所產生的停頓。
  分代演算法將按照物件的生命週期長短劃分成兩個部分,分割槽演算法將整個堆空間劃分成連續的不同小區間。
  每一個小區間都獨立使用,獨立回收。這種演算法的好處是可以控制一次回收多少個小區間。

在這裡插入圖片描述
寫在最後
注意,這些只是基本的演算法思路,實際GC實現過程要複雜的多,目前還在發展中的前沿GC都是複合演算法,並且並行和併發兼備。

相關文章