15-垃圾回收相关算法
垃圾回收相關算法
標記階段:引用計數算法(Java未使用)
垃圾標記階段:對象存活判斷
在堆里存放著幾乎所有的Java對象實例,在GC執行垃圾回收之前,首先需要區分出內存中哪些是存活對象,哪些是已經死亡的對象。只有被標記為己經死亡的對象,GC才會在執行垃圾回收時,釋放掉其所占用的內存空間,因此這個過程我們可以稱為垃圾標記階段。
那么在JVM中究竟是如何標記一個死亡對象呢?簡單來說,當一個對象已經不再被任何的存活對象繼續引用時,就可以宣判為已經死亡。
判斷對象存活一般有兩種方式:引用計數算法和可達性分析算法。
引用計數算法(Reference Counting)比較簡單,對每個對象保存一個整型的引用計數器屬性。用于記錄對象被引用的情況。
對于一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要對象A的引用計數器的值為0,即表示對象A不可能再被使用,可進行回收。
優點:實現簡單,垃圾對象便于辨識;判定效率高,回收沒有延遲性。(即不需要等GC時才回收)
缺點:它需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷。
每次賦值都需要更新計數器,伴隨著加法和減法操作,這增加了時間開銷。
 引用計數器有一個嚴重的問題,即無法處理循環引用的情況。這是一條致命缺陷,導致在Java的垃圾回收器中沒有使用這類算法。
循環引用
當p的指針斷開的時候,內部的引用形成一個循環,這就是循環引用,從而造成內存泄漏
舉例
我們使用一個案例來測試Java中是否采用的是引用計數算法
/*** 引用計數算法測試*/ public class RefCountGC {// 這個成員屬性的唯一作用就是占用一點內存private byte[] bigSize = new byte[5*1024*1024];// 引用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;// 顯示的執行垃圾收集行為,判斷obj1 和 obj2是否被回收?System.gc();} }運行結果
[GC (System.gc()) [PSYoungGen: 15490K->808K(76288K)] 15490K->816K(251392K), 0.0061980 secs] [Times: user=0.00 sys=0.00, real=0.36 secs] [Full GC (System.gc()) [PSYoungGen: 808K->0K(76288K)] [ParOldGen: 8K->672K(175104K)] 816K->672K(251392K), [Metaspace: 3479K->3479K(1056768K)], 0.0045983 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] HeapPSYoungGen total 76288K, used 655K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000)eden space 65536K, 1% used [0x000000076b500000,0x000000076b5a3ee8,0x000000076f500000)from space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)to space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)ParOldGen total 175104K, used 672K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000)object space 175104K, 0% used [0x00000006c1e00000,0x00000006c1ea8070,0x00000006cc900000)Metaspace used 3486K, capacity 4496K, committed 4864K, reserved 1056768Kclass space used 385K, capacity 388K, committed 512K, reserved 1048576K我們能夠看到,上述進行了GC收集的行為,將上述的新生代中的兩個對象都進行回收了
PSYoungGen: 15490K->808K(76288K)] 15490K->816K(251392K)如果使用引用計數算法,那么這兩個對象將會無法回收。而現在兩個對象被回收了,說明Java使用的不是引用計數算法來進行標記的。
小結
引用計數算法,是很多語言的資源回收選擇,例如因人工智能而更加火熱的Python,它更是同時支持引用計數和垃圾收集機制。
具體哪種最優是要看場景的,業界有大規模實踐中僅保留引用計數機制,以提高吞吐量的嘗試。
Java并沒有選擇引用計數,是因為其存在一個基本的難題,也就是很難處理循環引用關系。Python如何解決循環引用?
手動解除:很好理解,就是在合適的時機,解除引用關系。
 使用弱引用weakref,weakref是Python提供的標準庫,旨在解決循環引用。(GC時將弱引用回收)
標記階段:可達性分析算法(Java使用)
概念
可達性分析算法:也可以稱為 根搜索算法、追蹤性垃圾收集
相對于引用計數算法而言,可達性分析算法不僅同樣具備實現簡單和執行高效等特點,更重要的是該算法可以有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生。
相較于引用計數算法,這里的可達性分析就是Java、C#選擇的。這種類型的垃圾收集通常也叫作追蹤性垃圾收集(Tracing Garbage Collection)
思路
所謂"GC Roots”根集合就是一組必須活躍的引用。
基本思路:
- 可達性分析算法是以根對象集合(GCRoots)為起始點,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達。
- 使用可達性分析算法后,內存中的存活對象都會被根對象集合直接或間接連接著,搜索所走過的路徑稱為引用鏈(Reference Chain)
- 如果目標對象沒有任何引用鏈相連,則是不可達的,就意味著該對象己經死亡,可以標記為垃圾對象。
- 在可達性分析算法中,只有能夠被根對象集合直接或者間接連接的對象才是存活對象。
官場上的裙帶關系,可達性分析在人類關系網中
GC Roots可以是哪些?
- 虛擬機棧中引用的對象 - 比如:各個線程被調用的方法中使用到的參數、局部變量等。
 
- 本地方法棧內JNI(通常說的本地方法)引用的對象方法區中類靜態屬性引用的對象 - 比如:Java類的引用類型靜態變量
 
- 方法區中常量引用的對象 - 比如:字符串常量池(string Table)里的引用
 
- 所有被同步鎖synchronized持有的對象
- Java虛擬機內部的引用。 - 基本數據類型對應的Class對象,一些常駐的異常對象(如:Nu11PointerException、outofMemoryError),系統類加載器。
 
- 反映java虛擬機內部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等。
總結
總結一句話就是,除了堆空間外的一些結構,比如 虛擬機棧、本地方法棧、方法區、字符串常量池 等地方對堆空間進行引用的,都可以作為GC Roots進行可達性分析
除了這些固定的GC Roots集合以外,根據用戶所選用的垃圾收集器以及當前回收的內存區域不同,還可以有其他對象“臨時性”地加入,共同構成完整GC Roots集合。比如:分代收集和局部回收(PartialGC)。
- 如果只針對Java堆中的某一塊區域進行垃圾回收(比如:典型的只針對新生代),必須考慮到內存區域是虛擬機自己的實現細節,更不是孤立封閉的,這個區域的對象完全有可能被其他區域(如老年代)的對象所引用,這時候就需要一并將關聯的區域(如老年代)對象也加入GC Roots集合中去考慮,才能保證可達性分析的準確性。
tupian1
小技巧
由于Root采用棧方式存放變量和指針,所以如果一個指針,它保存了堆內存里面的對象,但是自己又不存放在堆內存里面,那它就是一個Root。
注意
如果要使用可達性分析算法來判斷內存是否可回收,那么分析工作必須在一個能保障一致性的快照中進行。這點不滿足的話分析結果的準確性就無法保證。
這點也是導致GC進行時必須“stop The World”的一個重要原因。
即使是號稱(幾乎)不會發生停頓的CMS收集器中,枚舉根節點時也是必須要停頓的。
對象的finalization機制
Java語言提供了對象終止(finalization)機制來允許開發人員提供對象被銷毀之前的自定義處理邏輯。
當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象之前,總會先調用這個對象的finalize()方法。
finalize() 方法允許在子類中被重寫,用于在對象被回收時進行資源釋放。通常在這個方法中進行一些資源釋放和清理的工作,比如關閉文件、套接字和數據庫連接等。
注意
永遠不要主動調用某個對象的finalize()方法I應該交給垃圾回收機制調用。理由包括下面三點:
- 在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方法只會被調用一次。
 
上圖就是我們看到的Finalizer線程
代碼演示
我們使用重寫 finalize()方法,然后在方法的內部,重寫將其存放到GC Roots中
/*** 測試Object類中finalize()方法* 對象復活場景**/ public class CanReliveObj {// 類變量,屬于GC Roots的一部分public static CanReliveObj canReliveObj;//finalize方法只會被調用一次@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("調用當前類重寫的finalize()方法");canReliveObj = this;//當前待回收的對象在finalize()方法中與引用鏈上的一個對象canReliveObj建立了聯系}public static void main(String[] args) throws InterruptedException {canReliveObj = new CanReliveObj();canReliveObj = null;System.gc();//調用垃圾回收器System.out.println("-----------------第一次gc操作------------");// 因為Finalizer線程的優先級比較低,暫停2秒,以等待它Thread.sleep(2000);if (canReliveObj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}System.out.println("-----------------第二次gc操作------------");canReliveObj = null;System.gc();// 下面代碼和上面代碼是一樣的,但是 這次自救卻失敗了Thread.sleep(2000);if (canReliveObj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}} }最后運行結果
-----------------第一次gc操作------------ 調用當前類重寫的finalize()方法 obj is still alive -----------------第二次gc操作------------ obj is dead在進行第一次清除的時候,我們會執行finalize方法,然后 對象 進行了一次自救操作,但是因為finalize()方法只會被調用一次,因此第二次該對象將會被垃圾清除。
MAT與JProfiler的GC Roots溯源
MAT是什么?
MAT是Memory Analyzer的簡稱,它是一款功能強大的Java堆內存分析器。用于查找內存泄漏以及查看內存消耗情況。
MAT是基于Eclipse開發的,是一款免費的性能分析工具。
大家可以在http://www.eclipse.org/mat/下載并使用MAT
獲取dump文件
####方式一: 命令行使用 jmap
####方式二: 使用JVIsualVM導出
捕獲的heap dump文件是一個臨時文件,關閉JVisualVM后自動刪除,若要保留,需要將其另存為文件。可通過以下方法捕獲heap dump:
在左側“Application"(應用程序)子窗口中右擊相應的應用程序,選擇Heap Dump(堆Dump)。
在Monitor(監視)子標簽頁中點擊Heap Dump(堆Dump)按鈕。本地應用程序的Heap dumps作為應用程序標簽頁的一個子標簽頁打開。同時,heap dump在左側的Application(應用程序)欄中對應一個含有時間戳的節點。
右擊這個節點選擇save as(另存為)即可將heap dump保存到本地。
使用MAT打開Dump文件
打開后,我們就可以看到有哪些可以作為GC Roots的對象
 
里面我們能夠看到有一些常用的Java類,然后Thread線程。
JProfiler的GC Roots溯源
我們在實際的開發中,一般不會查找全部的GC Roots,可能只是查找某個對象的整個鏈路,或者稱為GC Roots溯源,這個時候,我們就可以使用JProfiler對程序中內存泄漏的問題進行監控。
 (比如,有一些對象我們不再使用了,但是做可達性分析時,仍發現點他是可達的,則不能被GC成功回收,這就是內存泄漏,最終會造成OOM。此時,我們就應該找到該對象對應到GC Root的那一支鏈路,找到合適的位置切斷它與GC Roots的聯系,使其能夠被成功回收)
如何判斷什么原因造成OOM
當我們程序出現OOM的時候,我們就需要進行排查,我們首先使用下面的例子進行說明
/*** 內存溢出排查* -Xms8m -Xmx8m -XX:HeapDumpOnOutOfMemoryError//出現OOM時,我們會生成一個堆的Dump文件*/ public class HeapOOM {// 創建1M的文件byte [] buffer = new byte[1 * 1024 * 1024];public static void main(String[] args) {ArrayList<HeapOOM> list = new ArrayList<>();int count = 0;try {while (true) {list.add(new HeapOOM());count++;}} catch (Exception e) {e.getStackTrace();System.out.println("count:" + count);}} }上述代碼就是不斷的創建一個1M小字節數組,然后讓內存溢出,我們需要限制一下內存大小,同時使用HeapDumpOnOutOfMemoryError將出錯時候的dump文件輸出(.hprof文件)
-Xms8m -Xmx8m -XX:HeapDumpOnOutOfMemoryError我們將生成的dump文件打開,然后點擊Biggest Objects就能夠看到超大對象
然后我們通過線程,還能夠定位到哪里出現OOM
清除階段:標記-清除算法
當成功區分出內存中存活對象和死亡對象后,GC接下來的任務就是執行垃圾回收,釋放掉無用對象所占用的內存空間,以便有足夠的可用內存空間為新對象分配內存。目前在JVM中比較常見的三種垃圾收集算法是
- 標記一清除算法(Mark-Sweep)
- 復制算法(copying)
- 標記-壓縮算法(Mark-Compact)
背景
標記-清除算法(Mark-Sweep)是一種非常基礎和常見的垃圾收集算法,該算法被J.McCarthy等人在1960年提出并并應用于Lisp語言。
執行過程
當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也被稱為stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除
- 標記:Collector(垃圾回收器)從引用根節點開始遍歷,標記所有被引用的對象。一般是在對象的Header中記錄為可達對象。 - 標記的是引用的對象,不是垃圾!!
 
- 清除:Collector對堆內存從頭到尾進行線性的遍歷,如果發現某個對象在其Header中沒有標記為可達對象,則將其回收
回顧:對象的內存布局(對象頭)
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-NiQmby7D-1602353389407)(images/image-20200709151033237.png)]
 [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-K5p6LAZO-1602353389408)(images/image-20200709152801713.png)]
缺點
- 標記清除算法的效率不算高(標記和清除都需要經過兩次O(N)的全遍歷,當然,標記不是真正的全遍歷,只是遞歸的遍歷所有可達對象,清除就是真正的對堆內存全遍歷)
- 在進行GC的時候,需要停止整個應用程序,用戶體驗較差
- 這種方式清理出來的空閑內存是不連續的,產生內存碎片,需要維護一個空閑列表(為對象分配內存使用空閑列表分配)
什么是清除?
這里所謂的清除并不是真的置空,而是把需要清除的對象地址保存在空閑的地址列表里,只是沒有指針指向他了。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放覆蓋原有地址的內容。
關于空閑列表是在為對象分配內存的時候 提過
- 如果內存規整 - 采用指針碰撞的方式進行內存分配
 
- 如果內存不規整 - 虛擬機需要維護一個列表
- 空閑列表分配
 
清除階段:復制(Copying)算法
背景
為了解決標記-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky于1963年發表了著名的論文,“使用雙存儲區的Lisp語言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky在該論文中描述的算法被人們稱為復制(Copying)算法,它也被M.L.Minsky本人成功地引入到了Lisp語言的一個實現版本中。
核心思想
將活著的內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中,之后清除正在使用的內存塊中的所有對象,交換兩個內存的角色,最后完成垃圾回收
把可達的對象,直接復制到另外一個區域中復制完成后,A區就沒有用了,里面的對象可以直接清除掉,其實當時講的新生代里面Survivor區的From和To區就用到了復制算法
優點
- 沒有標記和清除過程,實現簡單,運行高效
- 復制過去以后保證空間的連續性,不會出現“碎片”問題。(為對象分配內存使用指針碰撞)
缺點
- 此算法的缺點也是很明顯的,就是需要兩倍的內存空間。
- 對于G1這種分拆成為大量region的GC,復制而不是移動,意味著GC需要維護region之間對象引用關系,不管是內存占用或者時間開銷也不小
注意
如果系統中的存活對象很多,復制算法不會很理想。復制算法適用需要復制的存活對象數量并不會太大,或者說非常低才行(老年代大量的對象存活,那么復制的對象將會有很多,效率會很低。此外,老年代雖然空間比較大,但用于存儲大對象,如果使用復制算法,則每次可用空間就要砍一半,這就浪費很大,因此,老年代不適合用復制算法)
在新生代,大多對象都是“朝生夕死”,對常規應用的垃圾回收,一次通常可以回收70% - 99% 的內存空間。回收性價比很高。所以現在的商業虛擬機都是用這種收集算法回收新生代。
清除階段:標記-整理算法
背景
復制算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在新生代經常發生,但是在老年代,更常見的情況是大部分對象都是存活對象。如果依然使用復制算法,由于存活對象較多,復制的成本也將很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
標記一清除算法的確可以應用在老年代中,但是該算法不僅執行效率低下,而且在執行完內存回收后還會產生內存碎片,所以JvM的設計者需要在此基礎之上進行改進。標記-壓縮(Mark-Compact)算法由此誕生。
1970年前后,G.L.Steele、C.J.Chene和D.s.Wise等研究者發布標記-壓縮算法。在許多現代的垃圾收集器中,人們都使用了標記-壓縮算法或其改進版本。
執行過程
第一階段和標記清除算法一樣,從根節點開始標記所有被引用對象
第二階段將所有的存活對象壓縮到內存的一端,按順序排放。之后,清理邊界外所有的空間。
標清和標整的區別
標記-壓縮算法的最終效果等同于標記-清除算法執行完成后,再進行一次內存碎片整理,因此,也可以把它稱為標記-清除-壓縮(Mark-Sweep-Compact)算法。
二者的本質差異在于標記-清除算法是一種非移動式的回收算法,標記-壓縮是移動式的。是否移動回收后的存活對象是一項優缺點并存的風險決策。可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閑列表顯然少了許多開銷。(為對象分配內存使用指針碰撞)
標整的優缺點
優點
- 消除了標記-清除算法當中,內存區域分散的缺點,我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可。
- 消除了復制算法當中,內存減半的高額代價。
缺點
- 從效率上來說,標記-整理算法要低于復制算法,更低于標記-清除算法。
- 移動對象的同時,如果對象被其他對象引用,則還需要調整引用的地址
- 移動過程中,需要全程暫停用戶應用程序。即:STW
小結
效率上來說,復制算法是當之無愧的老大,但是卻浪費了太多內存。
而為了盡量兼顧上面提到的三個指標,標記-整理算法相對來說更平滑一些,但是效率上不盡如人意,它比復制算法多了一個標記的階段,比標記-清除多了一個整理內存的階段。
| 速率 | 中等 | 最慢 | 最快 | 
| 空間開銷(內存利用率) | 少(但會堆積碎片) | 少(不堆積碎片) | 通常需要活對象的2倍空間(不堆積碎片) | 
| 移動對象(內存整齊度) | 否 (空閑列表分配對象地址) | 是 (碰撞指針法分配對象地址) | 是 (碰撞指針法分配對象地址) | 
綜合我們可以找到,沒有最好的算法,只有最合適的算法
分代收集算法(具體問題具體分析)
前面所有這些算法中,并沒有一種算法可以完全替代其他算法,它們都具有自己獨特的優勢和特點。分代收集算法應運而生。
分代收集算法,是基于這樣一個事實:不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點使用不同的回收算法,以提高垃圾回收的效率。
在Java程序運行的過程中,會產生大量的對象,其中有些對象是與業務信息相關,比如Http請求中的Session對象、線程、Socket連接,這類對象跟業務直接掛鉤,因此生命周期比較長。但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,比如:string對象,由于其不變類的特性,系統會產生大量的這些對象,有些對象甚至只用一次即可回收。
目前幾乎所有的GC都采用分代收集算法執行垃圾回收的
在HotSpot中,基于分代的概念,GC所使用的內存回收算法必須結合年輕代和老年代各自的特點。
- 年輕代(Young Gen)
年輕代特點:區域相對老年代較小,對象生命周期短、存活率低,回收頻繁。
這種情況復制算法的回收整理,速度是最快的。復制算法的效率只和當前存活對象大小有關,因此很適用于年輕代的回收。而復制算法內存利用率不高的問題,通過hotspot中的兩個survivor的設計得到緩解(新生代:老年代=1:2,新生代中Eden:S0:S1=8:1:1,因此,只有1/30的空間被“浪費”,因此,并未“浪費”太多空間,可以接受)。
- 老年代(Tenured Gen)
老年代特點:區域較大,對象生命周期長、存活率高,回收不及年輕代頻繁。
這種情況存在大量存活率高的對象,復制算法明顯變得不合適。一般是由標記-清除或者是標記-清除與標記-整理的混合實現。
- Mark階段的開銷與存活對象的數量成正比。
- Sweep階段的開銷與所管理區域的大小成正相關。(清除遍歷全堆)
- compact階段的開銷與存活對象的數據成正比。
以HotSpot中的CMS回收器為例,CMS是基于Mark-Sweep實現的,對于對象的回收效率很高。而對于碎片問題,CMS采用基于Mark-Compact算法的Serial old回收器作為補償措施:當內存回收不佳(碎片導致的Concurrent Mode Failure時),將采用serial old執行FullGC以達到對老年代內存的整理。
分代的思想被現有的虛擬機廣泛使用。幾乎所有的垃圾回收器都區分新生代和老年代
增量收集算法(低延遲)
概述
上述現有的算法,在垃圾回收過程中,應用軟件將處于一種stop the World的狀態。在stop the World狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程序會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。為了解決這個問題,即對實時垃圾收集算法的研究直接導致了增量收集(Incremental Collecting)算法的誕生。
如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接著切換到應用程序線程。依次反復,直到垃圾收集完成。
總的來說,增量收集算法的基礎仍是傳統的標記-清除和復制算法。增量收集算法通過對線程間沖突的妥善處理,允許垃圾收集線程以分階段的方式完成標記、清理或復制工作
缺點
使用這種方式,由于在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
分區算法
一般來說,在相同條件下,堆空間越大,一次GC時所需要的時間就越長,有關GC產生的停頓也越長。為了更好地控制GC產生的停頓時間,將一塊大的內存區域分割成多個小塊,根據目標的停頓時間,每次合理地回收若干個小區間,而不是整個堆空間,從而減少一次GC所產生的停頓。
分代算法將按照對象的生命周期長短劃分成兩個部分,分區算法將整個堆空間劃分成連續的不同小區間region。
 每一個小區間都獨立使用,獨立回收。這種算法的好處是可以控制一次回收多少個小區間。
寫到最后
注意,這些只是基本的算法思路,實際GC實現過程要復雜的多,目前還在發展中的前沿GC都是復合算法,并且并行和并發兼備。
總結
以上是生活随笔為你收集整理的15-垃圾回收相关算法的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: 10-对象实例化、内存布局与访问定位
- 下一篇: 16-垃圾回收相关概念
