目錄
- 一、參考計數法
- 二、根搜索演算法
- 三、標記·清除演算法( Mark-Sweep )
- 四、復制演算法 (Copying)
- 五、標記-壓縮演算法 (Mark-Compact)
- 六、增量演算法 (Incremental Collecting )
- 七、分代收集演算法 (Generational Collecting)
目前有兩種比較常見的垃圾標記演算法,分別是 參考計數演算法 和 根搜索演算法 ,參考計數器在微軟的 COM 組件技術 中、 Adodb 的 ActionScript3 中都有使用,
一、參考計數法
參考計數法(Refernce Counting ) 在 GC 執行垃圾回收之前,首先需要區分出記憶體中哪些是存活物件,哪些是已經死亡的物件,只有被標記為己經死亡的物件, 才會在執行垃圾回收時,釋放掉其所占用的記憶體空間,因此這個程序我們可以稱為 垃圾標記階段,
參考計數器的實作很簡單,對于一個物件A,只要有任何一個物件參考了A,則A的參考計數器就加1,當參考失效時,參考計數器就減1,只要物件A的參考計數器的值為0,則物件A就不可能再被使用,也就是說,參考計數器的實作只需要為每個物件配置一個整形的計數器即可,參考計數器演算法的一大優勢就是不用等待記憶體不夠用的時候,才進行垃圾的回收,完全可以在賦值操作的同時檢查計數器是否為0,如果是的話就可以立即回收,
但是參考計數器有一個嚴重的問題,即無法處理回圈參考的情況,一個簡單的回圈參考問題的描述如下 有物件A和物件B,物件A中含有物件B的參考,物件B中含有物件A的參考,此時,物件A和物件B的參考計數器都不為0,但是在系統中卻不存在任何第3個物件參考了 ,也就是說,A和B是應該被回收的垃圾物件,但由于垃圾物件間相互參考,從而使垃圾回收器無法識別,引起 記憶體泄漏 ,
如下圖所示,構造了一個串列,將最后一個元素的 next 屬性指向第一個元素,即參考第一個元素,從而構成回圈參考,這個時候如果將串列的頭 head 賦值為 null,此時串列的各個元素的計數器都不為0,同時也失去了對串列的參考控制,從而導致串列元素不能被回收,

參考計數器擁有一些特性,首先它需要單獨的欄位存盤計數器,這樣的做法增加了存盤空間的開銷,其次,每次賦值都需要更新計數器,這增加了時間開銷,再者,垃圾物件便于辨識,只要計數器為0,就可作為垃圾回收,接下來它能方便及時地回收垃圾,沒有延遲性,最后不能解決回圈參考的問題,正是由于最后一條致命缺陷,導致在 Java 的垃圾回收器中沒有使用這類演算法,
參考資料:
《深入理解JVM & G1 GC》
本文文字內容偏多,為避免閱讀疲勞,請添加助理VX:C18173184271,備注一下CSDN+作業年限!免費獲取
以便能夠充分理解學習!
二、根搜索演算法
Hotspot 和大部分 JVM 都是使用 根搜索演算法 作為垃圾標記的演算法實作,前面介紹過的參考計數演算法盡管實作簡單,執行效率也不錯,但是該演算法本身卻存在一個較大的弊端,甚至會影響到垃圾標記的準確性,由于參考計數演算法會為程式中的每一個物件都創建一個私有的參考計數器,當目標物件被其他存活物件參考時,參考計數器中的值則會加1,不再參考時便會減 1,當參考計數器中的值為0的時候,就意味著該物件己經不再被任何存活物件參考,可以被標記為垃圾物件,采用這種方式看起來似乎沒有任何問題,但是如果一些明顯已經死亡了的物件盡管沒有被任何的存活物件參考,但是它們彼此之間卻存在相互參考時,參考計數器中的值則永遠不會為0,這樣便會導致 GC 在執行記憶體回收時永遠無法釋放掉這種無用物件所占用的記憶體空間,極有可能引發記憶體泄漏,
相對于參考計數演算法而言,根搜索演算法不僅同樣具備 實作簡單 和 執行高效 等特點,更重要的是該演算法可以有效地解決在參考計數演算法中一些己經死亡的物件因相互參考而導致的無法正確被標記的問題,防止記憶體泄漏的發生,簡單來說,根搜索演算法是以根物件集合為起始點,按照從上至下的方式搜索被根物件集合所連接的目標物件是否可達(使用根搜索演算法后,記憶體中的存活物件都會被根物件集合直接或間接連接著),如果目標物件不可達,就意味著該物件己經死亡,便可以在 instanceOopDesc Mark World 中將其標記為垃圾物件,在根搜索演算法中,只有能夠被根物件集合直接或者間接連接的物件才是存活物件,在 Hotspot 中,根物件集合中包含了5個元素, Java 堆疊內的物件參考、本地方法堆疊內的物件參考、運行時常量池中的物件參考、方法區中類靜態屬性的物件參考以及與 個類對應的唯一資料型別的 Class 物件,
注解①:Hotspot 代碼中用 instanceOopDesc 來表示 Java 物件,而該類繼承 oopDesc ,所以 Hotspot 中的 Java 物件 也自然擁有 oopDesc 所宣告的頭部,
注意,在根搜索演算法中不可達的物件,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記程序,如果物件在進行根搜索后發現沒有與 GC Roots 相連接的參考鏈,那它將會被第一次標記井且進行一次篩選,篩選的條件是此物件是否有必要執行 finalize() 方法,當物件沒有覆寫 finalize() 方法,或者 finalize() 方法 已經被虛擬機呼叫過,虛擬機將這兩種情況都視為 “ 沒有必要執行 ” ,如果這個物件被判定為有必要執行 finalize() 方法,那么這個物件將會被放置在一個名為 F-Queue 的佇列之中,并在稍后由一條由虛擬機自動建立的、低優先級的 Finalizer 執行緒 去執行,這里所謂的“執行”是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束,這樣做的原因是,如果 個物件在 finalize()方法 中執行緩慢,或者發生了死回圈(更極端的情況),很可能會導致 F-Queue 佇列 中的其他物件永久處于等待狀態,甚至導致整個記憶體回收系統崩潰, finalize() 方法 是物件逃脫死亡命運的最后一次機會,稍后 GC 將對 F- Queue 中的物件進行第二次小規模的標記,如果物件要在 finalize () 中成功拯救自己一一只要重新與參考鏈上的任何一個物件建立關聯即可,譬如把自己(this 關鍵字)賦值給某個類變數或物件的成員變數,那在第二次標記時它將被移除出“即將回收”的集合,如果物件這時候還沒有逃脫,那它就真的離死不遠了,從下面的代碼中可以看到一個物件的 finalize ()被執行,但是它仍然可以存活,
逃脫回收實驗
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive");
}
@Override
protected void finalize() throws Throwable {
super.finalize() ;
System.out.println("finalize mehtod executed !");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC ();
//物件第 次成功拯救自己
SAVE_HOOK = null ;
System.gc();
//因為 Finalizer 方法優先級很低,暫停 秒,以等待它
Thread.sleep(5OO);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive() ;
} else {
System.out.println("no , i am dead");
}
//下面這段代碼與上面的完全相同 ,但是這次自救卻失敗了
SAVE_HOOK = null ;
System.gc();
//因為 Finalizer 方法優先級很低,暫停 0.5 秒,以等待它
Thread .sleep(500) ;
if (SAVE_HOOK ! = null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead");
}
}
}
輸出如下面代碼所示:逃脫回收實驗運行輸出
finalize mehtod executed!
yes, i am still alive
no , i am dead
從上面代碼的運行結果可以看到, SAVE_HOOK 物件 的 finalize() 方法 確實被 GC 收集器 觸發過,并且在被收集前成功逃脫了,另外一個值得注意的地方就是,代碼中有兩段完全樣的代碼片段,執行結果卻是一次逃脫成功,一次失敗,這是因為任何一個物件的 finalize() 方法 都只會被系統自動呼叫一次,如果物件面臨下一次回收,它的 finalize() 方法 不會被再次執行,因此第2段代碼的自救行動失敗了,
三、標記·清除演算法( Mark-Sweep )
當成功區分出記憶體中存活物件和死亡物件后, GC 接下來的任務就是執行垃圾回收,釋放掉無用物件所占用的記憶體空間,以便有足夠的可用記憶體空間為新物件分配記憶體,目前在 JVM中比較常見的三種垃圾收集演算法是 標記-清除演算法( Mark-Sweep )、復制演算法( Copying )、標記-壓縮演算法( Mark-Compact ),在介紹三種演算法之前,我們先來通過下圖看看它們之間的區別,

標記-清除演算法( Mark-Sweep ) 是一種非常基礎和常見的垃圾收集演算法,該演算法被 J.McCarthy 等人在 1960 年提出并成功地發明井應用于 Lisp 語言,以餐巾紙作為示例, 午餐程序中,餐廳的所有人都根據自己的需要取用餐巾紙, 當垃圾收集機器人想收集廢舊餐巾紙的時候,它讓所有用餐的人先停下來,然后,依次詢問餐廳里的每一個人:“你正在用餐巾紙嗎?你用的是哪一張餐巾紙?”機器人根據每個人的回答將人們正在使用的餐巾紙畫上記號,詢問程序結束后,機器人在餐廳里尋找所有散落在餐桌上且沒有記號的餐巾紙(這些顯然都是用過的廢舊餐巾紙),把它們統統扔到垃圾箱里,
回到演算法本身,演算法涉及幾個概念,先來了解 mutator 和 collector ,這兩個名詞經常在垃圾收集演算法中出現, collector 指的就是 垃圾收集器 ,而 mutator 是指 除了垃圾收集器之外的部分 ,比如說我們的應用程式本身, mutator 的職責一般是 NEW (分配記憶體)、READ (從記憶體中讀取內容)、 WRITE (將內容寫入記憶體),而 collector 就是回收不再使用的記憶體來供 mutator 進行 NEW 操作的使用, mutator 根物件 一般指的是分配在堆記憶體之外,可以直接被 mutator 直接訪問到的物件,一般是指靜態/全域變數以及 ThreadLocal 變數,
注解②: Java 中,存盤在 Java .lang.ThreadLocal 中的變數和分配在枝上的變數方法內部的臨時變數等都屬于此類,
標記-清除演算法 將垃圾回收分為兩個階段,標記階段 和 清除階段 ,在標記階段, collector 從 mutator 根物件 開始進行遍歷,對從 mutator 根物件 可以訪問到的物件都打上一個標識,一般是在物件的 header 中,將其記錄為可達物件,而在清除階段, collector 對堆記憶體( heap memory ) 從頭到尾進行線性的遍歷,如果發現某個物件沒有標記為可達物件,通過讀取物件的 header 資訊,則將其回收,一種可行的實作是,在標記階段首先通過根節點,標記所有從根節點開始的可達物件,因此,未被標記的物件就是未被參考的垃圾物件,然后,在清除階段,清除所有未被標記的物件,
前面說過,標記-清除演算法 的執行程序分為 “標記” 和 “清除” 兩大階段,這種分步執行的思路奠定了現代垃圾收集演算法的思想基礎,與參考計數演算法不同的是,標記-清除演算法不需要運行環境監測每一次記憶體分配和指標操作,而只要在“標記”階段中跟蹤每一個指標變數的指向,用類似思路實作的垃圾收集器也常被后人統稱為 跟蹤收集器( Tracing Collector ),
標記-清除演算法 最大的問題是存在大量的空間碎片,因為回收后的空間是不連續的,在物件的堆空間分配程序中,尤其是大物件的記憶體分配,不連續的記憶體空間的作業效率要低于連續的空間,
相對于另外兩種記憶體回收演算法而 標記-清除演算法(Mark-Sweep) 不僅 執行效率低下,更重要的是,由于被執行記憶體回收的無用物件所占用的記憶體空間有可能是一些不連續的記憶體塊,不可避免地會產生一些記憶體碎片,從而導致后續沒有足夠的可用記憶體空間分配給較大的物件,
四、復制演算法 (Copying)
為了解決 標記-清除演算法 在垃圾收集效率方面的缺陷,M.L.Minsky 于1963 年發表了著名的論文, “一種使用雙存盤區的 Lisp 語言垃圾收集器 (A LISP Garbage Collector Algorithm Using Serial Secondary Storage )” ,M.L.Minsky 在該論文中描述的演算法被人們稱為 復制演算法,它也被 M.L.Minsky 本人成功地引入到了 Lisp 個實作版本中,
標記-清除演算法 后來被引入 JVM 中,提升了 GC 在垃圾標記和記憶體釋放這兩個階段的執行效率,還是采用之前的餐廳示例,餐廳被垃圾收集機器人分成南區和北區兩個大小完全相同的部分,午餐時,所有人都先在南區用餐(因為空間有限,用餐人數自然也將減少一半) ,用餐時可以隨意使用餐巾紙,當垃圾收集機器人認為有必要回收廢舊餐巾紙時,它會要求所有用餐者以最快的速度從南區轉移到北區,同時隨身攜帶自己正在使用的餐巾紙,等所有人都轉移到北區之后,垃圾收集機器人只要簡單地把南區中所有散落的餐巾紙扔進垃圾箱就算完成任務了,下一次垃圾收集的作業程序也大致類似,唯一的不同只是人們的轉移方向變成了從北區到南區,如此回圈往復,每次垃圾收集都只需簡單地轉移(也就是復制)一次,垃圾收集速度無與倫比一一當然,對于用餐者往返奔波于南北兩區之間的辛勞 ,垃圾收集機器人是絕不會流露出絲毫憐憫的,
回到演算法本身,復制演算法首先將活著的記憶體空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的記憶體中的存活物件復制到未被使用的記憶體塊中,之后清除正在使用的記憶體塊中的所有物件,交換兩個記憶體的角色,最后完成垃圾回收,
如果系統中的垃圾物件很多,復制演算法需要復制的存活物件數量并不會太大,因此在真正需要垃圾回收的時刻,復制演算法的效率是很高的,又由于物件在垃圾回收程序中統一被復制到新的記憶體空間中,因此,可確保回收后的記憶體空間是沒有碎片的,該演算法的缺點是將系統記憶體折半,
Java 年輕代串行垃圾回收器中使用了復制演算法的思想,年輕代分為 Eden 空間、 From 空間 、 To 空間 3個部分,其中 From 空間 和 To 空間 可以視為用于復制的兩塊大小相同、地位相等,且可進行角色互換的空間塊, From To 空間 也稱為 Survivor 空間,即 幸存者空間,用于存放未被回收的物件,
在垃圾回收時, Eden 空間 中的存活物件會被復制到未使用的 Survivor 空間 中(假設是 To),正在使用的 Survivor 空間(假設是 From )中的年輕物件也會被復制到 To 空間中(大物件,或者老年物件會直接進入老年代,如果 To 空間己滿,則物件也會直接進入老年代),此時, Eden 空間 和 From 空間 中的剩余物件就是垃圾物件,可以直接清空, To 空間則存放此次回收后的存活物件,這種改進的復制演算法既保證了空間的連續性,又避免了大量的記憶體空間浪費,
基于分代的概念, Java 堆區如果還要更進一步細分的話,還可以劃分為年輕代( YoungGen) 和 老年代( OldGen ),其中年輕代又可以被劃分為 Eden 空間、 From Survivor 空間 和 To Survivor 空間,在 HotSpot 中, Eden 空間 和另外兩個 Survivor 空間 默認所占的比例是8:1, 當然開發員可以通過選項“-XX SurvivorRatio ”調整這個空間比例 ,當執行一次 MinorGC (年輕代的垃圾回收)時, Eden 空間中的存活物件會被復制到 To 空間內,井且之前已經經歷過一次 MinorGC 并在 From 空間中存活下來的物件如果還年輕的話同樣也會被復制到 To 空間內,需要注意的是,在滿足兩種特殊情況下, Eden From 空間 中的存活物件將不會被復制到 To 空間內,首先是如果存活物件的分代年齡超過選項“ XX: MaxTenuringThreshold ”所指定的閥值時,將會直接晉升到老年代中,其次當 To 空間的容量達到閥值時,存活物件同樣也是直接晉升到老年代中當所有的存活物件都被復制到 To 空間或者晉升到老年代后,剩下的均為垃圾物件,這就意味GC 可以對這些已經死亡了的物件執行一次 MinorGC ,釋放掉其所占用的記憶體空間,
當執行完 Minor GC 之后, Eden 空間 和 From 空間 將會被清空,而存活下來的物件則會被全部存盤在 To 空間內 ,接下來 From 空間和 To 空間將會互換位置,其實復制演算法無非就是使 To Survivor 空間作為一個臨時的空間交換角色,務必需要保證兩塊空間中一塊必須是空的,這就是復制演算法,盡管復制演算法能夠高效執行 MinorGC ,但是它卻并不適用于老年代中的記憶體回收,因為老年代中物件的生命周期都比較長,甚至在某些極端的情況下還能夠與 JVM 的生命周期保持一致,所以如果老年代也采用復制演算法執行記憶體回收,不僅需要額外的時間和空間,而且還會導致較多的復制操作影響到 GC 的執行效率,
總的來說,由于 JVM 中的絕大多數物件都是瞬時狀態的,生命周期非常短暫,所以復制演算法被廣泛應用于年輕代中,磁區、復制的思路不僅大幅提高了垃圾收集的效率,而且也將原本繁紛復雜的記憶體分配演算法變得前所未有的簡明和扼要(既然每次記憶體回收都是對整個半區的回收,記憶體分配時也就不用考慮記憶體碎片等復雜情況,只要移動堆頂指標,按順序分配記憶體就可以了),這簡直是個奇跡!不過,任何奇跡的出現都有一定的代價,在垃圾收集技術中,復制演算法提高效率的代價是人為地將可用記憶體縮小了一半,
五、標記-壓縮演算法 (Mark-Compact)
標記-清除演算法 的確可以應用在老年代中,但是該演算法不僅執行效率低下,而且在執行完記憶體回收后還會產生記憶體碎片,所以 JVM 的設計者在此基礎之上進行了改進,標記-壓縮演算法 由此誕生,標記-壓縮演算法 是 標記-清除演算法 和 復制演算法 的有機結合,把標記-清除演算法在記憶體占用上的優點和復制演算法在執行效率上的特長綜合起來,這是所有人都希望看到的結果,不過,兩種垃圾收集演算法的整合并不像 1加1 等于2 那樣簡單,必須引入一些全新的思路,
1970 年前后, G. L. Steele、C. J. Chene 和 D.S. Wise 等研究者陸續找到了正確的方向,標記-壓縮演算法 的輪廓也逐漸清晰了起來,還是采用之前的餐廳示例,在我們熟悉的餐廳里,這一次,垃圾收集機器人不再把餐廳分成兩個南北區域了,需要執行垃圾收集任務時,機器人先執行 標記-清除演算法 的第一個步驟,為所有使用中的餐巾紙畫好標記,然后,機器人命令所有就餐者帶上有標記的餐巾紙向餐廳的南面集中,同時把沒有標記的廢舊餐巾紙扔向餐廳北面,這樣來,機器人只需要站在餐廳北面,懷抱垃圾箱,迎接撲面而來的廢舊餐巾紙就行了,
回到演算法本身, 成功標記出記憶體中的垃圾物件后,標記-壓縮演算法 會將所有的存活物件都移動到一個規整且連續的記憶體空間中,然后執行 Full GC (老年代的垃圾回收,或者被稱為 or GC ) 回收無用物件所占用的記憶體空間,當成功執行壓縮之后,己用和未用的記憶體都各自一邊,彼此之間維系著一個記錄下一次分配起始點的標記指標,當為新物件分配記憶體時,則可以使用 指標碰撞( Bump the Pointer ) 技術修改指標的偏移量將新物件分配在第一個空閑記憶體位置上,為新物件分配記憶體帶來便捷,
在 HotSpot 中,基于分代的概念, GC 所使用的記憶體回收演算法必須結合年輕代和老年代各自的特點,簡單來說,就是針對不同的代空間,從而結合使用不同的垃圾收集演算法,為年輕代選擇的垃圾收集演算法通常是以速度優先,因為年輕代中所存盤的瞬時物件生命周期非常短暫,可以有針對性地使用 復制演算法,因此執行 Minor GC 時, 一定要保持高效和快速,而年輕代中的生存空間通常都比較小,所以回收年輕代時一定會非常頻繁,但老年代通常使用更節省記憶體的回收演算法,因為老年代中所存盤的物件生命周期都非常長,并且老年代占據了大部分的堆空間,所以老年代的 Full GC 并不會跟年輕代的 Minor GC 一樣頻繁,不過一旦程式中發生一次 Full GC ,將會耗費更長的時間來完成,那么在老年代中使用標記-清除演算法或者標記-壓縮演算法執行垃圾回收將會是不錯的選擇,
復制演算法 的高效性是建立在存活物件少、垃圾物件多的前提下的,這種情況在年輕代經常發生,但是在老年代更常見的情況是大部分物件都是存活物件,如果依然使用復制演算法,由于存活的物件較多,復制的成本也將很高,標記-壓縮演算法 是一種老年代的回收演算法,它在 標記-清除演算法 的基礎上做了一些優化,也首先需要從根節點開始對所有可達物件做一次標記,但之后,它并不是簡單地清理未標記的物件,而是將所有的存活物件壓縮到記憶體的 端,之后,清理邊界外所有的空間,這種方法既避免了碎片的產生,又不需要兩塊相同的記憶體空間,因此,其性價比比較高,
標記-壓縮演算法 的總體執行效率高于 標記-清除演算法,又不像 復制演算法 那樣需要犧牲一半的存盤空間,這顯然是一種非常理想的結果,在許多現代的垃圾收集器中,人們都使用了 標記-壓縮演算法 或其改進版本,
六、增量演算法 (Incremental Collecting )
在垃圾回收程序中,應用軟體將處于一種 Stop the World 的狀態,在 Stop the World 狀態下,應用程式所有的執行緒都會掛起,暫停一切正常的作業,等待垃圾回收的完成,如果垃圾回收時間過長,應用程式會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性,為了解決這個問題,即對實時垃圾收集演算法的研究直接導致了增量收集演算法的誕生,
最初,為了進行實時垃圾收集,可以設計一個多行程的運行環境,比如用一個行程執行垃
圾收集作業,另一個行程執行程式代碼,這樣一來,垃圾收集作業看上去就仿佛是在后臺悄悄完成的,不會打斷程式代碼的運行,在收集餐巾紙的例子中,這一思路可以被理解為垃圾收集機器人在人們用餐的同時尋找廢棄的餐巾紙并將它們扔到垃圾箱里,這個看似簡單的思路會在設計和實作時碰上行程間沖突的難題,比如說,如果垃圾收集行程包括標記和清除兩個作業階段,那么,垃圾收集器在第一階段中辛辛苦苦標記出的結果很可能被另一個行程中的記憶體操作代碼修改得面目全非,以至于第二階段的作業沒有辦法開展,
M. L. Minsky 和 D. E. Knuth 對實時垃圾收集程序中的技術難點進行了早期的研究, G. L. Steele 1975 年發表了題為 “多行程整理的垃圾收集 Multiproc sing Compactifying Garbage Collection ” 的論文,描述了一種被后人稱為 “Minsky-Knuth-Steele 演算法” 的實時垃圾收集演算法, E.W.Dijkstra、L.Lamport、R.R.Fenichel 和 J.C.Yochelson 等人也相繼在此領域做出了各自的貢獻, 1978 年, H.G .Baker 發表了 “串行計算機上的實時表處理技術 (List Processing in Real Time on a Serial Computer)” 一文,系統闡述了多行程環境下用于垃圾收集的 增量收集演算法 ,
增量演算法 的基本思想是,如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓 垃圾收集執行緒 和 應用程式執行緒 交替執行,每次,垃圾收集執行緒只收集一小片區域的記憶體空間,接著切換到應用程式執行緒,依次反復,直到垃圾收集完成,使用這種方式,由于在垃圾回收程序中,間斷性地還執行了應用程式代碼,所以能減少系統的停頓時間,但是,因為執行緒切換和背景關系轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降,
總的來說,增量收集演算法的基礎仍是傳統的標記 清除和復制演算法,增量收集演算法通過對進
程間沖突的妥善處理,允許垃圾收集行程以分階段的方式完成標記、清理或復制作業,
七、分代收集演算法 (Generational Collecting)
1980 年前后,善于在研究中使用統計分析知識的技術人員發現,大多數記憶體塊的生存周期都比較短,垃圾收集器應當把更多的精力放在檢查和清理新分配的記憶體塊上,還是餐巾紙的例子,如果垃圾收集機器人足夠聰明,事先摸清了餐廳里每個人在用餐時使用餐巾紙的習慣一一比如有些人喜歡在用餐前后各用掉一張餐巾紙,有的人喜歡自始至終攥著一張餐巾紙不放,有的人則每打一個噴嗖就去用一張餐巾紙一一機器人就可以制定出更完善的餐巾紙回收計劃,并總是在人們剛扔掉餐巾紙沒多久就把垃圾撿走,這種基于統計學原理的做法當然可以讓餐廳的整潔度成倍提高,D. E. Knuth、T. Knight、G. Sussman 和 R. Stallman 等人對記憶體垃圾的分類處理做了最早的研究, 1983 年, H. Lieberman 和 C.Hewitt 發表了題為 “基于物件壽命的一種實時垃圾收集器( Real-Time Garbage Collector Based on the Lifetimes of ects ” 的論文,這篇著名的論文標志著分代收集演算法的正式誕生,此后,在 H. G. Baker、R. L. Hudson、 J.E. B. Moss 等人的共同努力下,分代收集演算法逐漸成為了垃圾收集領域里的主流技術,
根據垃圾回收物件的特性,使用合適的演算法回收,分代就是基于這種思想,它將記憶體區間根據物件的特點分成幾塊,根據每塊記憶體區間的特點,使用不同的回收演算法以提高垃圾回收的效率,
以 Hotspot 虛擬機 為例,它將所有的新建物件都放入稱為年輕代的記憶體區域,年輕代的特點是物件會很快回收,因此,在年輕代就選擇效率較高的復制演算法,當一個物件經過幾次回收后依然存活,物件就會被放入稱為老年代的記憶體空間,在老年代中,幾乎所有的物件都是經過幾次垃圾回收后依然得以幸存的,因此,可以認為這些物件在一段時期內,甚至在應用程式的整個生命周期中,將是常駐記憶體的,如果依然使用復制演算法回收老年代,將需要復制大量物件,再加上老年代的回收性價比也要低于年輕代,因此這種做法也是不可取的,根據分代的思想,可以對老年代的回收使用與年輕代不同的 標記-壓縮演算法,以提高垃圾回收效率,
總的來說,分代收集演算法是基于對物件生命周期分析后得出的垃圾回收演算法,它把物件分為年輕代、老年代、持久代,對不同生命周期的物件使用不同的演算法(上述方式中的一個)進行回收, JVM 垃圾回收器(從 J2SE1.2 開始)都是使用此演算法的,
如果你需要這份完整版的
《深入理解JVM & G1 GC》,只需你多多支持我這篇文章,
多多支持,即可免費獲取資料——三連之后(承諾:100%免費)
快速入手通道:添加助理VX:
C18173184271,備注一下CSDN+作業年限!免費獲取!誠意滿滿!!!
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/253518.html
標籤:其他
上一篇:常見的幾種排序
下一篇:Filter-Policy


