潮汕網(wǎng)站建設(shè)antnw網(wǎng)頁設(shè)計需要學(xué)什么
對象存活判斷
在堆里存放著幾乎所有的 Java 對象實例,在 GC 執(zhí)行垃圾回收之前,首先需要區(qū)分出內(nèi)存中哪些是存活對象,哪些是已經(jīng)死亡的對象。只有被標(biāo)記為己經(jīng)死亡的對象,GC 才會在執(zhí)行垃圾回收時,釋放掉其所占用的內(nèi)存空間,因此這個過程我們可以稱為垃圾標(biāo)記階段。
那么在 JVM 中究竟是如何標(biāo)記一個死亡對象呢?簡單來說,當(dāng)一個對象已經(jīng)不再被任何的存活對象繼續(xù)引用時,就可以宣判為已經(jīng)死亡。
判斷對象存活一般有兩種方式:引用計數(shù)算法和可達(dá)性分析算法。
12.1. 標(biāo)記階段:引用計數(shù)算法
12.1.1 什么是引用計數(shù)算法
引用計數(shù)算法(Reference Counting)比較簡單,對每個對象保存一個整型的引用計數(shù)器屬性。用于記錄對象被引用的情況。
對于一個對象 A,只要有任何一個對象引用了 A,則 A 的引用計數(shù)器就加 1;當(dāng)引用失效時,引用計數(shù)器就減 1。只要對象 A 的引用計數(shù)器的值為 0,即表示對象 A 不可能再被使用,可進(jìn)行回收。
優(yōu)點:實現(xiàn)簡單,垃圾對象便于辨識;判定效率高,回收沒有延遲性。
缺點:
- 它需要單獨的字段存儲計數(shù)器,這樣的做法增加了存儲空間的開銷。
- 每次賦值都需要更新計數(shù)器,伴隨著加法和減法操作,這增加了時間開銷。
- 引用計數(shù)器有一個嚴(yán)重的問題,即無法處理循環(huán)引用的情況。這是一條致命缺陷,導(dǎo)致在 Java 的垃圾回收器中沒有使用這類算法。
12.1.2 循環(huán)引用
當(dāng) p 的指針斷開的時候,內(nèi)部的引用形成一個循環(huán),這就是循環(huán)引用
舉例 :測試 Java 中是否采用的是引用計數(shù)算法
/*** -XX:+PrintGCDetails* 證明:java使用的不是引用計數(shù)算法* @author shkstart* @create 2020 下午 2:38*/
public class RefCountGC {//這個成員屬性唯一的作用就是占用一點內(nèi)存private byte[] bigSize = new byte[5 * 1024 * 1024];//5MBObject 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;//顯式的執(zhí)行垃圾回收行為//這里發(fā)生GC,obj1和obj2能否被回收?System.gc();}
}
上述進(jìn)行了 GC 收集的行為,所以可以證明 JVM 中采用的不是引用計數(shù)器的算法
12.1.3 小結(jié)
引用計數(shù)算法,是很多語言的資源回收選擇,例如因人工智能而更加火熱的 Python,它更是同時支持引用計數(shù)和垃圾收集機(jī)制。
具體哪種最優(yōu)是要看場景的,業(yè)界有大規(guī)模實踐中僅保留引用計數(shù)機(jī)制,以提高吞吐量的嘗試。
Java 并沒有選擇引用計數(shù),是因為其存在一個基本的難題,也就是很難處理循環(huán)引用關(guān)系。
Python 如何解決循環(huán)引用?
- 手動解除:很好理解,就是在合適的時機(jī),解除引用關(guān)系。
- 使用弱引用 weakref,weakref 是 Python 提供的標(biāo)準(zhǔn)庫,旨在解決循環(huán)引用。
12.2. 標(biāo)記階段:可達(dá)性分析算法
12.2.1 可達(dá)性分析算法(根搜索算法、追蹤性垃圾收集)
相對于引用計數(shù)算法而言,可達(dá)性分析算法不僅同樣具備實現(xiàn)簡單和執(zhí)行高效等特點,更重要的是該算法可以有效地解決在引用計數(shù)算法中循環(huán)引用的問題,防止內(nèi)存泄漏的發(fā)生。
相較于引用計數(shù)算法,這里的可達(dá)性分析就是 Java、C#選擇的。這種類型的垃圾收集通常也叫作追蹤性垃圾收集(Tracing Garbage Collection)
所謂"GCRoots”根集合就是一組必須活躍的引用。
12.2.2 基本思路
- 可達(dá)性分析算法是以根對象集合(GCRoots)為起始點,按照從上至下的方式搜索被根對象集合所連接的目標(biāo)對象是否可達(dá)。
- 使用可達(dá)性分析算法后,內(nèi)存中的存活對象都會被根對象集合直接或間接連接著,搜索所走過的路徑稱為引用鏈(Reference Chain)
- 如果目標(biāo)對象沒有任何引用鏈相連,則是不可達(dá)的,就意味著該對象己經(jīng)死亡,可以標(biāo)記為垃圾對象。
- 在可達(dá)性分析算法中,只有能夠被根對象集合直接或者間接連接的對象才是存活對象。
只要被GC Roots里面的引用 直接 或間接指向的對象,就是存活對象,否則就是要被回收的對象
在 Java 語言中,GC Roots 包括以下幾類元素:
- 虛擬機(jī)棧中引用的對象
- 比如:各個線程被調(diào)用的方法中使用到的參數(shù)、局部變量等。
- 本地方法棧內(nèi) JNI(通常說的本地方法)引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 比如:Java 類的引用類型靜態(tài)變量
- 方法區(qū)中常量引用的對象
- 比如:字符串常量池(String Table)里的引用
- 所有被同步鎖 synchronized 持有的對象
- Java 虛擬機(jī)內(nèi)部的引用。
- 基本數(shù)據(jù)類型對應(yīng)的 Class 對象,一些常駐的異常對象(如:NullPointerException、OutOfMemoryError),系統(tǒng)類加載器。
- 反映 java 虛擬機(jī)內(nèi)部情況的 JMXBean、JVMTI 中注冊的回調(diào)、本地代碼緩存等。
除了這些固定的 GC Roots 集合以外,根據(jù)用戶所選用的垃圾收集器以及當(dāng)前回收的內(nèi)存區(qū)域不同,還可以有其他對象“臨時性”地加入,共同構(gòu)成完整 GC Roots 集合。比如:分代收集和局部回收
(PartialGC)。
如果只針對 Java 堆中的某一塊區(qū)域進(jìn)行垃圾回收(比如:典型的只針對新生代
),必須考慮到內(nèi)存區(qū)域是虛擬機(jī)自己的實現(xiàn)細(xì)節(jié),更不是孤立封閉的,這個區(qū)域的對象完全有可能被其他區(qū)域的對象所引用,這時候就需要一并將關(guān)聯(lián)的區(qū)域?qū)ο?#xff08;比如老年代)也加入 GCRoots 集合中去考慮,才能保證可達(dá)性分析的準(zhǔn)確性。
小技巧:由于 Root 采用棧方式存放變量和指針,所以如果一個指針,它保存了堆內(nèi)存里面的對象,但是自己又不存放在堆內(nèi)存里面,那它就是一個 Root。
注意
如果要使用可達(dá)性分析算法來判斷內(nèi)存是否可回收,那么分析工作必須在一個能保障一致性
的快照中進(jìn)行。這點不滿足的話分析結(jié)果的準(zhǔn)確性就無法保證。
這點也是導(dǎo)致 GC 進(jìn)行時必須“stop The World”的一個重要原因。
- 即使是號稱(幾乎)不會發(fā)生停頓的 CMS 收集器中,枚舉根節(jié)點時也是必須要停頓的。
12.3. 對象的 finalization 機(jī)制
Java 語言提供了對象終止(finalization)機(jī)制來允許開發(fā)人員提供對象被銷毀之前的自定義處理邏輯。
當(dāng)垃圾回收器發(fā)現(xiàn)沒有引用指向一個對象,即:垃圾回收此對象之前,總會先調(diào)用這個對象的 finalize()方法。
finalize() 方法允許在子類中被重寫,用于在對象被回收時進(jìn)行資源釋放。通常在這個方法中進(jìn)行一些資源釋放和清理的工作,比如關(guān)閉文件、套接字和數(shù)據(jù)庫連接等。
永遠(yuǎn)不要主動調(diào)用某個對象的 finalize()方法 , 應(yīng)該交給垃圾回收機(jī)制調(diào)用。理由包括下面三點:
- 在 finalize()時可能會導(dǎo)致對象復(fù)活。
- finalize()方法的執(zhí)行時間是沒有保障的,它完全由 GC 線程【一個優(yōu)先級非常低的finalizer線程】決定,極端情況下,若不發(fā)生 GC,則 finalize()方法將沒有執(zhí)行機(jī)會。
- 一個糟糕的 finalize()會嚴(yán)重影響 GC的性能。
從功能上來說,finalize()方法與 C++中的析構(gòu)函數(shù)比較相似,但是 Java 采用的是基于垃圾回收器的自動內(nèi)存管理機(jī)制,所以 finalize()方法在本質(zhì)上不同于 C++中的析構(gòu)函數(shù)。
由于 finalize()方法的存在,虛擬機(jī)中的對象一般處于三種可能的狀態(tài)。
12.3.1 生存還是死亡?
如果從所有的根節(jié)點都無法訪問到某個對象,說明對象己經(jīng)不再使用了。一般來說,此對象需要被回收。但事實上,也并非是“非死不可”的,這時候它們暫時處于“緩刑”階段。一個無法觸及的對象有可能在某一個條件下“復(fù)活”自己,如果這樣,那么對它的回收就是不合理的,為此,定義虛擬機(jī)中的對象可能的三種狀態(tài)。如下:
- 可觸及的:從根節(jié)點開始,可以到達(dá)這個對象。
- 可復(fù)活的:對象的所有引用都被釋放【不可達(dá)】,但是對象有可能在 finalize()中復(fù)活。
- 不可觸及的:對象的 finalize()被調(diào)用,并且沒有復(fù)活,那么就會進(jìn)入不可觸及狀態(tài)。不可觸及的對象不可能被復(fù)活,因為finalize()只會被調(diào)用一次。
以上 3 種狀態(tài)中,是由于 inalize()方法的存在,進(jìn)行的區(qū)分。只有在對象不可觸及時才可以被回收。
12.3.2 具體過程
判定一個對象 objA 是否可回收,至少要經(jīng)歷兩次標(biāo)記過程:
- 如果對象 objA 到 GC Roots 沒有引用鏈,則進(jìn)行第一次標(biāo)記。
- 進(jìn)行篩選,判斷此對象是否有必要執(zhí)行 finalize()方法
- 如果對象 objA 沒有重寫 finalize()方法,或者 finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過,則虛擬機(jī)視為“沒有必要執(zhí)行”,objA 被判定為不可觸及的。
- 如果對象 objA 重寫了 finalize()方法,且還未執(zhí)行過,那么 objA 會被插入到 F-Queue 隊列中,由一個虛擬機(jī)自動創(chuàng)建的、低優(yōu)先級的 Finalizer 線程觸發(fā)其 finalize()方法執(zhí)行。
- finalize()方法是對象逃脫死亡的最后機(jī)會,稍后 GC 會對 F-Queue 隊列中的對象進(jìn)行第二次標(biāo)記。如果 objA 在 finalize()方法中與引用鏈上的任何一個對象建立了聯(lián)系,那么在第二次標(biāo)記時,objA 會被移出“即將回收”集合。之后,對象會再次出現(xiàn)沒有引用存在的情況。在這個情況下,finalize 方法不會被再次調(diào)用,對象會直接變成不可觸及的狀態(tài),也就是說,一個對象的 finalize 方法只會被調(diào)用一次。
舉例
/*** 測試Object類中finalize()方法,即對象的finalization機(jī)制。** @author shkstart* @create 2020 下午 2:57*/
public class CanReliveObj {public static CanReliveObj obj;//類變量,屬于 GC Root//此方法只能被調(diào)用一次@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("調(diào)用當(dāng)前類重寫的finalize()方法");obj = this;//當(dāng)前待回收的對象在finalize()方法中與引用鏈上的一個對象obj建立了聯(lián)系}public static void main(String[] args) {try {obj = new CanReliveObj();// 對象第一次成功拯救自己obj = null;System.gc();//調(diào)用垃圾回收器System.out.println("第1次 gc");// 因為Finalizer線程優(yōu)先級很低,暫停2秒,以等待它Thread.sleep(2000);if (obj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}System.out.println("第2次 gc");// 下面這段代碼與上面的完全相同,但是這次自救卻失敗了obj = null;System.gc();// 因為Finalizer線程優(yōu)先級很低,暫停2秒,以等待它Thread.sleep(2000);if (obj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}} catch (InterruptedException e) {e.printStackTrace();}}
}
運行結(jié)果
調(diào)用當(dāng)前類重寫的finalize()方法
第1次 gc
obj is still alive
第2次 gc
obj is dead
在第一次 GC 時,執(zhí)行了 finalize 方法,對象復(fù)活。但 finalize()方法只會被調(diào)用一次,所以第二次該對象被 GC 標(biāo)記并清除了。
12.4. MAT 與 JProfiler 的 GC Roots 溯源
12.4.1 MAT 是什么?
MAT 是 Memory Analyzer 的簡稱,它是一款功能強(qiáng)大的 Java 堆內(nèi)存分析器。用于查找內(nèi)存泄漏以及查看內(nèi)存消耗情況。
MAT 是基于 Eclipse 開發(fā)的,是一款免費的性能分析工具。
大家可以在 http://www.eclipse.org/mat/ 下載并使用 MAT
接下來,演示如何查看GC Roots都會以下面的代碼案例
/*** @author shkstart shkstart@126.com* @create 2020 16:28*/
public class GCRootsTest {public static void main(String[] args) {List<Object> numList = new ArrayList<>();Date birth = new Date();for (int i = 0; i < 100; i++) {numList.add(String.valueOf(i));try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("數(shù)據(jù)添加完畢,請操作:");new Scanner(System.in).next();numList = null;birth = null;System.out.println("numList、birth已置空,請操作:");new Scanner(System.in).next();System.out.println("結(jié)束");}
}
12.4.2 獲取 dump 文件
方式一:命令行使用 jmap
方式二:使用 JVisualVM 導(dǎo)出
捕獲的 heap dump 文件是一個臨時文件,關(guān)閉 JVisualVM 后自動刪除,若要保留,需要將其另存為文件。
可通過以下方法捕獲 heap dump:
- 在左側(cè)“Application"(應(yīng)用程序)子窗口中右擊相應(yīng)的應(yīng)用程序,選擇 Heap Dump(堆 Dump)。
- 在 Monitor(監(jiān)視)子標(biāo)簽頁中點擊 Heap Dump(堆 Dump)按鈕。
本地應(yīng)用程序的 Heap dumps 作為應(yīng)用程序標(biāo)簽頁的一個子標(biāo)簽頁打開。同時,heap dump 在左側(cè)的 Application(應(yīng)用程序)欄中對應(yīng)一個含有時間戳的節(jié)點。
右擊這個節(jié)點選擇 save as(另存為)即可將 heap dump 保存到本地。
這里,我們分別在 數(shù)據(jù)添加完畢
和 numList、birth置空
后dump一次~
12.4.3 使用 MAT 打開 Dump 文件
打開方式:
效果展示:
進(jìn)行分析:
12.4.4 JProfiler 的 GC Roots 溯源
我們在實際的開發(fā)中,一般不會查找全部的 GC Roots,可能只是查找某個對象的整個鏈路,或者稱為 GC Roots 溯源,這個時候,我們就可以使用 JProfiler
12.4.5 JProfiler查看OOM的原因
/*** -Xms8m -Xmx8m -XX:+HeapDumpOnOutOfMemoryError** @author shkstart shkstart@126.com* @create 2020 15:29*/
public class HeapOOM {byte[] buffer = new byte[1 * 1024 * 1024];//1MBpublic static void main(String[] args) {ArrayList<HeapOOM> list = new ArrayList<>();int count = 0;try{while(true){list.add(new HeapOOM());count++;}}catch (Throwable e){System.out.println("count = " + count);e.printStackTrace();}}
}
運行結(jié)果:
分析Dump文件
關(guān)于JProfile更多的使用,后面調(diào)優(yōu)篇章詳細(xì)介紹~
12.5. 清除階段:標(biāo)記-清除算法
當(dāng)成功區(qū)分出內(nèi)存中存活對象和死亡對象后,GC 接下來的任務(wù)就是執(zhí)行垃圾回收,釋放掉無用對象所占用的內(nèi)存空間,以便有足夠的可用內(nèi)存空間為新對象分配內(nèi)存。
目前在 JVM 中比較常見的三種垃圾收集算法是標(biāo)記一清除算法(Mark-Sweep)、復(fù)制算法(copying)、標(biāo)記-壓縮算法(Mark-Compact)
12.5.1 標(biāo)記-清除算法(Mark-Sweep)
標(biāo)記-清除算法(Mark-Sweep)是一種非?;A(chǔ)和常見的垃圾收集算法,該算法被 J.McCarthy 等人在 1960 年提出并并應(yīng)用于 Lisp 語言。
12.5.2 執(zhí)行過程
當(dāng)堆中的有效內(nèi)存空間(available memory)被耗盡的時候,就會停止整個程序(也被稱為 stop the world),然后進(jìn)行兩項工作,第一項則是標(biāo)記,第二項則是清除
-
標(biāo)記:Collector 從引用根節(jié)點開始遍歷,標(biāo)記所有被引用的對象。一般是在對象的 Header 中記錄為可達(dá)對象【非垃圾對象】。
-
清除:Collector 對堆內(nèi)存從頭到尾進(jìn)行線性的遍歷,如果發(fā)現(xiàn)某個對象在其 Header 中沒有標(biāo)記為可達(dá)對象,則將其回收
12.5.3 缺點
- 標(biāo)記清除算法的效率不算高【需要兩次遍歷】
- 在進(jìn)行 GC 的時候,需要停止整個應(yīng)用程序,用戶體驗較差
- 這種方式清理出來的空閑內(nèi)存是不連續(xù)的,產(chǎn)生內(nèi)碎片,需要維護(hù)一個空閑列表
12.5.4 何為清除?
這里所謂的清除并不是真的置空,而是把需要清除的對象地址保存在空閑的地址列表里。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放覆蓋原有的地址。
12.6. 清除階段:復(fù)制算法
12.6.4 復(fù)制算法
為了解決標(biāo)記-清除算法在垃圾收集效率方面的缺陷,M.L.Minsky 于 1963 年發(fā)表了著名的論文,“使用雙存儲區(qū)的 Lisp 語言垃圾收集器 CA LISP Garbage Collector Algorithm Using Serial Secondary Storage)”。M.L.Minsky 在該論文中描述的算法被人們稱為復(fù)制(Copying)算法,它也被 M.L.Minsky 本人成功地引入到了 Lisp 語言的一個實現(xiàn)版本中。
12.6.2 核心思想
將活著的內(nèi)存空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內(nèi)存中的存活對象復(fù)制到未被使用的內(nèi)存塊中,之后清除正在使用的內(nèi)存塊中的所有對象,交換兩個內(nèi)存的角色,最后完成垃圾回收
12.6.3 優(yōu)缺點
優(yōu)點:
- 沒有標(biāo)記和清除過程,實現(xiàn)簡單,運行高效
- 復(fù)制過去以后保證空間的連續(xù)性,不會出現(xiàn)“碎片”問題。
缺點:
- 此算法的缺點也是很明顯的,就是需要兩倍的內(nèi)存空間。
- 對于 G1 這種分拆成為大量 region 的 GC,復(fù)制而不是移動,意味著 GC 需要維護(hù) region 之間對象引用關(guān)系,不管是內(nèi)存占用或者時間開銷也不小【其實就是,當(dāng)堆區(qū)對象從A區(qū)復(fù)制到B后,局部變量表里面的引用也需要變化】
注意:如果系統(tǒng)中的垃圾對象很多,復(fù)制算法不會很理想。因為復(fù)制算法需要復(fù)制的存活對象數(shù)量并不會太大,或者說非常低才行
12.6.4 應(yīng)用場景
在新生代【對象朝生夕死】,對常規(guī)應(yīng)用的垃圾回收,一次通??梢曰厥?70% - 99% 的內(nèi)存空間?;厥招詢r比很高。所以現(xiàn)在的商業(yè)虛擬機(jī)都是用這種收集算法回收新生代。
為什么老年代不用復(fù)制算法?
1.老年代的存活對象較多,復(fù)制成本較高。
2.老年代本身的空間就很大,但是使用復(fù)制算法會直接砍掉一半,內(nèi)存浪費嚴(yán)重~
12.7. 清除階段:標(biāo)記-壓縮(整理)算法
12.7.1 標(biāo)記-壓縮(或標(biāo)記-整理、Mark-Compact)算法
復(fù)制算法的高效性是建立在存活對象少、垃圾對象多的前提下的。這種情況在新生代經(jīng)常發(fā)生,但是在老年代,更常見的情況是大部分對象都是存活對象。如果依然使用復(fù)制算法,由于存活對象較多,復(fù)制的成本也將很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
標(biāo)記一清除算法的確可以應(yīng)用在老年代中,但是該算法不僅執(zhí)行效率低下,而且在執(zhí)行完內(nèi)存回收后還會產(chǎn)生內(nèi)存碎片,所以 JVM 的設(shè)計者需要在此基礎(chǔ)之上進(jìn)行改進(jìn)。標(biāo)記-壓縮(Mark-Compact)算法由此誕生。
1970 年前后,G.L.Steele、C.J.Chene 和 D.s.Wise 等研究者發(fā)布標(biāo)記-壓縮算法。在許多現(xiàn)代的垃圾收集器中,人們都使用了標(biāo)記-壓縮算法或其改進(jìn)版本。
12.7.2 執(zhí)行過程
- 第一階段和標(biāo)記清除算法一樣,從根節(jié)點開始標(biāo)記所有被引用對象
- 第二階段將所有的存活對象壓縮到內(nèi)存的一端,按順序排放。
- 之后,清理邊界外所有的空間。
標(biāo)記-壓縮算法的最終效果等同于標(biāo)記-清除算法執(zhí)行完成后,再進(jìn)行一次內(nèi)存碎片整理,因此,也可以把它稱為標(biāo)記-清除-壓縮(Mark-Sweep-Compact)算法。
二者的本質(zhì)差異在于標(biāo)記-清除算法是一種非移動式的回收算法,標(biāo)記-壓縮是移動式的。是否移動回收后的存活對象是一項優(yōu)缺點并存的風(fēng)險決策??梢钥吹?#xff0c;標(biāo)記的存活對象將會被整理,按照內(nèi)存地址依次排列,而未被標(biāo)記的內(nèi)存會被清理掉。如此一來,當(dāng)我們需要給新對象分配內(nèi)存時,JVM 只需要持有一個內(nèi)存的起始地址即可,這比維護(hù)一個空閑列表顯然少了許多開銷。
指針碰撞(Bump the Pointer)
如果內(nèi)存空間以規(guī)整和有序的方式分布,即已用和未用的內(nèi)存都各自一邊,彼此之間維系著一個記錄下一次分配起始點的標(biāo)記指針,當(dāng)為新對象分配內(nèi)存時,只需要通過修改指針的偏移量將新對象分配在第一個空閑內(nèi)存位置上,這種分配方式就叫做指針碰撞(Bump tHe Pointer)。
12.7.3 優(yōu)缺點
優(yōu)點:
- 消除了標(biāo)記-清除算法當(dāng)中,內(nèi)存區(qū)域分散的缺點,我們需要給新對象分配內(nèi)存時,JVM 只需要持有一個內(nèi)存的起始地址即可。
- 消除了復(fù)制算法當(dāng)中,內(nèi)存減半的高額代價。
缺點:
- 從效率上來說,標(biāo)記-整理算法要低于復(fù)制算法。
- 移動對象的同時,如果對象被其他對象引用,則還需要調(diào)整引用的地址
- 移動過程中,需要全程暫停用戶應(yīng)用程序。即:STW
12.8. 小結(jié)
Mark-Sweep | Mark-Compact | Copying | |
---|---|---|---|
速率 | 中等 | 最慢 | 最快 |
空間開銷 | 少(但會堆積碎片) | 少(不堆積碎片) | 通常需要活對象的 2 倍空間(不堆積碎片) |
移動對象 | 否 | 是 | 是 |
效率上來說,復(fù)制算法是當(dāng)之無愧的老大,但是卻浪費了太多內(nèi)存。
而為了盡量兼顧上面提到的三個指標(biāo),標(biāo)記-整理算法相對來說更平滑一些,但是效率上不盡如人意,它比復(fù)制算法多了一個標(biāo)記的階段,比標(biāo)記-清除多了一個整理內(nèi)存的階段
難道就沒有一種最優(yōu)算法嗎?
回答:無,沒有最好的算法,只有最合適的算法。
12.9. 分代收集算法
前面所有這些算法中,并沒有一種算法可以完全替代其他算法,它們都具有自己獨特的優(yōu)勢和特點。分代收集算法應(yīng)運而生。
分代收集算法,是基于這樣一個事實:不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點使用不同的回收算法,以提高垃圾回收的效率。
在 Java 程序運行的過程中,會產(chǎn)生大量的對象,其中有些對象是與業(yè)務(wù)信息相關(guān),比如Http 請求中的 Session 對象、線程、Socket 連接,這類對象跟業(yè)務(wù)直接掛鉤,因此生命周期比較長。但是還有一些對象,主要是程序運行過程中生成的臨時變量,這些對象生命周期會比較短,比如:String 對象,由于其不變類的特性,系統(tǒng)會產(chǎn)生大量的這些對象,有些對象甚至只用一次即可回收。
目前幾乎所有的 GC 都采用分代手機(jī)算法執(zhí)行垃圾回收的。
在 HotSpot 中,基于分代的概念,GC 所使用的內(nèi)存回收算法必須結(jié)合年輕代和老年代各自的特點。
12.9.1 年輕代(Young Gen)
年輕代特點:區(qū)域相對老年代較小,對象生命周期短、存活率低,回收頻繁。
這種情況復(fù)制算法的回收整理,速度是最快的。復(fù)制算法的效率只和當(dāng)前存活對象大小有關(guān),因此很適用于年輕代的回收。而復(fù)制算法內(nèi)存利用率不高的問題,通過 hotspot 中的兩個 survivor 的設(shè)計得到緩解。
12.9.2 老年代(Tenured Gen)
老年代特點:區(qū)域較大,對象生命周期長、存活率高,回收不及年輕代頻繁。
這種情況存在大量存活率高的對象,復(fù)制算法明顯變得不合適。一般是由標(biāo)記-清除或者是標(biāo)記-清除與標(biāo)記-整理的混合實現(xiàn)。
- Mark 階段的開銷與存活對象的數(shù)量成正比。
- Sweep 階段的開銷與所管理區(qū)域的大小成正相關(guān)。
- Compact 階段的開銷與存活對象的數(shù)據(jù)成正比。
以 HotSpot 中的 CMS 回收器為例,CMS 是基于 Mark-Sweep 實現(xiàn)的,對于對象的回收效率很高。而對于碎片問題,CMS 采用基于 Mark-Compact 算法的 Serial Old 回收器作為補(bǔ)償措施:當(dāng)內(nèi)存回收不佳(碎片導(dǎo)致的 Concurrent Mode Failure 時),將采用 Serial Old 執(zhí)行 Full GC 以達(dá)到對老年代內(nèi)存的整理。
分代的思想被現(xiàn)有的虛擬機(jī)廣泛使用。幾乎所有的垃圾回收器都區(qū)分新生代和老年代
12.X. 增量收集算法、分區(qū)算法
12.X.1 增量收集算法
上述現(xiàn)有的算法,在垃圾回收過程中,應(yīng)用軟件將處于一種 Stop the World 的狀態(tài)。在 Stop the World 狀態(tài)下,應(yīng)用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應(yīng)用程序會被掛起很久,將嚴(yán)重影響用戶體驗或者系統(tǒng)的穩(wěn)定性。為了解決這個問題,即對實時垃圾收集算法的研究直接導(dǎo)致了增量收集(Incremental Collecting)算法的誕生。
基本思想
如果一次性將所有的垃圾進(jìn)行處理,需要造成系統(tǒng)長時間的停頓,那么就可以讓垃圾收集線程和應(yīng)用程序線程交替執(zhí)行
。每次,垃圾收集線程只收集一小片區(qū)域的內(nèi)存空間,接著切換到應(yīng)用程序線程。依次反復(fù),直到垃圾收集完成。
總的來說,增量收集算法的基礎(chǔ)仍是傳統(tǒng)的標(biāo)記-清除和復(fù)制算法。增量收集算法通過對線程間沖突的妥善處理,允許垃圾收集線程以分階段的方式完成標(biāo)記、清理或復(fù)制工作
缺點
使用這種方式,由于在垃圾回收過程中,間斷性地還執(zhí)行了應(yīng)用程序代碼,所以能減少系統(tǒng)的停頓時間。但是,因為線程切換和上下文轉(zhuǎn)換的消耗,會使得垃圾回收的總體成本上升,造成系統(tǒng)吞吐量的下降。
12.X.2 分區(qū)算法
一般來說,在相同條件下,堆空間越大,一次 Gc 時所需要的時間就越長,有關(guān) GC 產(chǎn)生的停頓也越長。為了更好地控制 GC 產(chǎn)生的停頓時間==,將一塊大的內(nèi)存區(qū)域分割成多個小塊==,根據(jù)目標(biāo)的停頓時間,每次合理地回收若干個小區(qū)間,而不是整個堆空間,從而減少一次 GC 所產(chǎn)生的停頓。
分代算法將按照對象的生命周期長短劃分成兩個部分【年輕代、老年代】,分區(qū)算法將整個堆空間劃分成連續(xù)的不同小區(qū)間。
每一個小區(qū)間都獨立使用,獨立回收。這種算法的好處是可以控制一次回收多少個小區(qū)間。
12.X.3 寫到最后
注意,這些只是基本的算法思路,實際 GC 實現(xiàn)過程要復(fù)雜的多,目前還在發(fā)展中的前沿 GC 都是復(fù)合算法,并且并行和并發(fā)兼?zhèn)洹?/p>