Java垃圾回收機(jī)制的示例詳解
一、概述
說起垃圾收集(Garbage Collection,下文簡稱GC),有不少人把這項技術(shù)當(dāng)作Java語言的伴生產(chǎn) 物。事實上,垃圾收集的歷史遠(yuǎn)遠(yuǎn)比Java久遠(yuǎn),在1960年誕生于麻省理工學(xué)院的Lisp是第一門開始使 用內(nèi)存動態(tài)分配和垃圾收集技術(shù)的語言。當(dāng)Lisp還在胚胎時期時,其作者John McCarthy就思考過垃圾 收集需要完成的三件事情:
- 哪些內(nèi)存需要回收?
- 什么時候回收?
- 如何回收?
經(jīng)過半個世紀(jì)的發(fā)展,今天的內(nèi)存動態(tài)分配與內(nèi)存回收技術(shù)已經(jīng)相當(dāng)成熟,一切看起來都進(jìn)入 了“自動化”時代,那為什么我們還要去了解垃圾收集和內(nèi)存分配?
答案很簡單:當(dāng)需要排查各種內(nèi)存 溢出、內(nèi)存泄漏問題時,當(dāng)垃圾收集成為系統(tǒng)達(dá)到更高并發(fā)量的瓶頸時,我們就必須對這些“自動 化”的技術(shù)實施必要的監(jiān)控和調(diào)節(jié)。
Java內(nèi)存運 行時區(qū)域其中程序計數(shù)器、虛擬機(jī)棧、本地方法棧3個區(qū)域隨線程而生,隨線程而滅,棧 中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。每一個棧幀中分配多少內(nèi)存基 本上是在類結(jié)構(gòu)確定下來時就已知的,因此這幾個區(qū)域的內(nèi)存分配和回收都具備確定性, 在這幾個區(qū)域內(nèi)就不需要過多考慮如何回收的問題,當(dāng)方法結(jié)束或者線程結(jié)束時,內(nèi)存自然就跟隨著 回收了。
而Java堆和方法區(qū)這兩個區(qū)域則有著很顯著的不確定性:一個接口的多個實現(xiàn)類需要的內(nèi)存可能 會不一樣,一個方法所執(zhí)行的不同條件分支所需要的內(nèi)存也可能不一樣,只有處于運行期間,我們才 能知道程序究竟會創(chuàng)建哪些對象,創(chuàng)建多少個對象,這部分內(nèi)存的分配和回收是動態(tài)的。垃圾收集器 所關(guān)注的正是這部分內(nèi)存該如何管理,本文后續(xù)討論中的“內(nèi)存”分配與回收也僅僅特指這一部分內(nèi)存。
二、對象已死?
在堆里面存放著Java世界中幾乎所有的對象實例,垃圾收集器在對堆進(jìn)行回收前,第一件事情就 是要確定這些對象之中哪些還“存活”著,哪些已經(jīng)“死去”(“死去”即不可能再被任何途徑使用的對 象)了。
這個面試的時候也經(jīng)常會問到,依靠兩個算法來判斷對象是否存活:
1、引用計數(shù)算法
2、可達(dá)分析算法。
1.引用計數(shù)算法
判斷對象是否存活的算法是這樣的:在對象中添加一個引用計數(shù)器,每當(dāng)有一個地方 引用它時,計數(shù)器值就加一;當(dāng)引用失效時,計數(shù)器值就減一;任何時刻計數(shù)器為零的對象就是不可 能再被使用的。
客觀地說,引用計數(shù)算法(Reference Counting)雖然占用了一些額外的內(nèi)存空間來進(jìn)行計數(shù),但 它的原理簡單,判定效率也很高,很多領(lǐng)域都在用,但是,在Java 領(lǐng)域,至少主流的Java虛擬機(jī)里面都沒有選用引用計數(shù)算法來管理內(nèi)存。原因:單純的引用計數(shù) 就很難解決對象之間相互循環(huán)引用的問題。
代碼示例:
在下面testGC()方法:對象objA和objB都有字段instance,賦值令 objA.instance=objB及objB.instance=objA,除此之外,這兩個對象再無任何引用,實際上這兩個對象已 經(jīng)不可能再被訪問,但是它們因為互相引用著對方,導(dǎo)致它們的引用計數(shù)都不為零,引用計數(shù)算法也 就無法回收它們。
對于objA = null,objB = null應(yīng)該有的人會不理解為什么要這么做,咱們要測試的是屬性出現(xiàn)了互相引用,是否會被gc掉。那么我就得保證其他地方不再引用他,我們用的是main方法測試,所以需要設(shè)置為null。正常在java應(yīng)用是不需要的,線程執(zhí)行過后,局部變量將被銷毀。那也就不存在引用這一說了。
雖然手動的讓局部變量不在引用堆中的對象,但是在堆內(nèi)存當(dāng)中這兩個對象的屬性還是相互引用的。如果按照計數(shù)算法,他是不應(yīng)該被gc掉的。
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/*** 這個成員屬性的唯一意義就是占點內(nèi)存,以便能在GC日志中看清楚是否有回收過 */
private byte[] bigSize = new byte[2 * _1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假設(shè)在這行發(fā)生GC,objA和objB是否能被回收?
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
設(shè)置啟動參數(shù):輸出GC的詳細(xì)日志
-XX:+PrintGCDetails
運行結(jié)果:

從運行結(jié)果中可以清楚看到內(nèi)存回收日志中包含“9257K->823K”,意味著虛擬機(jī)并沒有因為這兩 個對象互相引用就放棄回收它們,這也從側(cè)面說明了Java虛擬機(jī)并不是通過引用計數(shù)算法來判斷對象 是否存活的。
2.可達(dá)性分析算法
當(dāng)前主流的商用程序語言(Java、C#,上溯至前面提到的古老的Lisp)的內(nèi)存管理子系統(tǒng),都是 通過可達(dá)性分析(Reachability Analysis)算法來判定對象是否存活的。這個算法的基本思路就是通過 一系列稱為“GC Roots”的根對象作為起始節(jié)點集,從這些節(jié)點開始,根據(jù)引用關(guān)系向下搜索,搜索過 程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個對象到GC Roots間沒有任何引用鏈相連, 或者用圖論的話來說就是從GC Roots到這個對象不可達(dá)時,則證明此對象是不可能再被使用的。
如圖所示,對象object 5、object 6、object 7雖然互有關(guān)聯(lián),但是它們到GC Roots是不可達(dá)的, 因此它們將會被判定為可回收的對象。

在Java技術(shù)體系里面,固定可作為GC Roots的對象包括以下幾種:
- 在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調(diào)用的方法堆棧中使用到的 參數(shù)、局部變量、臨時變量等。
- 在方法區(qū)中類靜態(tài)屬性引用的對象,譬如Java類的引用類型靜態(tài)變量。
- 在方法區(qū)中常量引用的對象,譬如字符串常量池(String Table)里的引用。
- 在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
- Java虛擬機(jī)內(nèi)部的引用,如基本數(shù)據(jù)類型對應(yīng)的Class對象,一些常駐的異常對象(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統(tǒng)類加載器。
- 所有被同步鎖(synchronized關(guān)鍵字)持有的對象。
- 反映Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊的回調(diào)、本地代碼緩存等。
- 除了這些固定的GC Roots集合以外,根據(jù)用戶所選用的垃圾收集器以及當(dāng)前回收的內(nèi)存區(qū)域不 同,還可以有其他對象“臨時性”地加入,共同構(gòu)成完整GC Roots集合。譬如后文將會提到的分代收集 和局部回收(Partial GC),如果只針對Java堆中某一塊區(qū)域發(fā)起垃圾收集時(如最典型的只針對新生 代的垃圾收集),必須考慮到內(nèi)存區(qū)域是虛擬機(jī)自己的實現(xiàn)細(xì)節(jié)(在用戶視角里任何內(nèi)存區(qū)域都是不 可見的),更不是孤立封閉的,所以某個區(qū)域里的對象完全有可能被位于堆中其他區(qū)域的對象所引 用,這時候就需要將這些關(guān)聯(lián)區(qū)域的對象也一并加入GC Roots集合中去,才能保證可達(dá)性分析的正確 性。
目前最新的幾款垃圾收集器無一例外都具備了局部回收的特征。如OpenJDK中的G1、Shenandoah、ZGC以及Azul的PGC、C4這些收集器。
3.四種引用
無論是通過引用計數(shù)算法判斷對象的引用數(shù)量,還是通過可達(dá)性分析算法判斷對象是否引用鏈可 達(dá),判定對象是否存活都和“引用”離不開關(guān)系。在JDK 1.2版之前,Java里面的引用是很傳統(tǒng)的定義: 如果reference類型的數(shù)據(jù)中存儲的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱該reference數(shù)據(jù)是代表 某塊內(nèi)存、某個對象的引用。一個對象在 這種定義下只有“被引用”或者“未被引用”兩種狀態(tài),對于描述一些“食之無味,棄之可惜”的對象就顯 得無能為力。譬如我們希望能描述一類對象:當(dāng)內(nèi)存空間還足夠時,能保留在內(nèi)存之中,如果內(nèi)存空 間在進(jìn)行垃圾收集后仍然非常緊張,那就可以拋棄這些對象——很多系統(tǒng)的緩存功能都符合這樣的應(yīng) 用場景。
在JDK 1.2版之后,Java對引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strongly Re-ference)、軟 引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強(qiáng) 度依次逐漸減弱。
- 強(qiáng)引用是最傳統(tǒng)的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關(guān)系。無論任何情況下,只要強(qiáng)引用關(guān)系還存在,垃圾收集器就永遠(yuǎn)不會回 收掉被引用的對象。
- 軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關(guān)聯(lián)著的對象,在系統(tǒng)將要發(fā)生內(nèi) 存溢出異常前,會把這些對象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒有足夠的內(nèi)存, 才會拋出內(nèi)存溢出異常。在JDK 1.2版之后提供了SoftReference類來實現(xiàn)軟引用。
- 弱引用也是用來描述那些非必須對象,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對象只 能生存到下一次垃圾收集發(fā)生為止。當(dāng)垃圾收集器開始工作,無論當(dāng)前內(nèi)存是否足夠,都會回收掉只 被弱引用關(guān)聯(lián)的對象。在JDK 1.2版之后提供了WeakReference類來實現(xiàn)弱引用。
- 虛引用也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關(guān)系。一個對象是否有虛引用的 存在,完全不會對其生存時間構(gòu)成影響,也無法通過虛引用來取得一個對象實例。為一個對象設(shè)置虛 引用關(guān)聯(lián)的唯一目的只是為了能在這個對象被收集器回收時收到一個系統(tǒng)通知。在JDK 1.2版之后提供 了PhantomReference類來實現(xiàn)虛引用。
4.生存還是死亡?
即使在可達(dá)性分析算法中判定為不可達(dá)的對象,也不是“非死不可”的,這時候它們暫時還處于“緩 刑”階段,要真正宣告一個對象死亡,至少要經(jīng)歷兩次標(biāo)記過程:如果對象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒 有與GC Roots相連接的引用鏈,那它將會被第一次標(biāo)記,隨后進(jìn)行一次篩選,篩選的條件是此對象是 否有必要執(zhí)行finalize()方法。假如對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用 過,那么虛擬機(jī)將這兩種情況都視為“沒有必要執(zhí)行”。
finalize()方法是Object的一個方法,所有的對象都是Object的子類。只不過語法上默認(rèn)沒有繼承Object。但是實際上是繼承的Object類。
如果這個對象被判定為確有必要執(zhí)行finalize()方法,那么該對象將會被放置在一個名為F-Queue的 隊列之中,并在稍后由一條由虛擬機(jī)自動建立的、低調(diào)度優(yōu)先級的Finalizer線程去執(zhí)行它們的finalize() 方法。這里所說的“執(zhí)行”是指虛擬機(jī)會觸發(fā)這個方法開始運行,但并不承諾一定會等待它運行結(jié)束。
這樣做的原因是,如果某個對象的finalize()方法執(zhí)行緩慢,或者更極端地發(fā)生了死循環(huán),將很可能導(dǎo) 致F-Queue隊列中的其他對象永久處于等待。
如果對 象要在finalize()中成功拯救自己 只要重新與引用鏈上的任何一個對象建立關(guān)聯(lián)即可,譬如把自己 (this關(guān)鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標(biāo)記時它將被移出“即將回收”的集 合;如果對象這時候還沒有逃脫,那基本上它就真的要被回收了。從下面代碼我們可以看到一個 對象的finalize()被執(zhí)行,但是它仍然可以存活。
一次對象自我拯救的演示:
此代碼演示了兩點:
1.對象可以在被GC時自我拯救。
2.這種自救的機(jī)會只有一次,因為一個對象的finalize()方法最多只會被系統(tǒng)自動調(diào)用一次
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 method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//對象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因為Finalizer方法優(yōu)先級很低,暫停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面這段代碼與上面的完全相同,但是這次自救卻失敗了
SAVE_HOOK = null;
System.gc();
// 因為Finalizer方法優(yōu)先級很低,暫停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
運行結(jié)果:

finalize()方法大家盡量避免使用它,它的運行代價高昂,不確定性大,無法保證各個對象的調(diào)用順序,如今已被官方明確聲明為 不推薦使用的語法。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、 更及時。
5.回收方法區(qū)
方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類型。回收廢棄常量與回收 Java堆中的對象非常類似。舉個常量池中字面量回收的例子,假如一個字符串“java”曾經(jīng)進(jìn)入常量池 中,但是當(dāng)前系統(tǒng)又沒有任何一個字符串對象的值是“java”,換句話說,已經(jīng)沒有任何字符串對象引用 常量池中的“java”常量,且虛擬機(jī)中也沒有其他地方引用這個字面量。如果在這時發(fā)生內(nèi)存回收,這個“java”常量就將會被系統(tǒng)清理出常量池。常量池中其他類(接 口)、方法、字段的符號引用也與此類似。
判定一個常量是否“廢棄”還是相對簡單,而要判定一個類型是否屬于“不再被使用的類”的條件就 比較苛刻了。需要同時滿足下面三個條件:
- 該類所有的實例都已經(jīng)被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
- 加載該類的類加載器已經(jīng)被回收,這個條件除非是經(jīng)過精心設(shè)計的可替換類加載器的場景,如 OSGi、JSP的重加載等,否則通常是很難達(dá)成的。
- 該類對應(yīng)的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方 法。
- Java虛擬機(jī)被允許對滿足上述三個條件的無用類進(jìn)行回收,這里說的僅僅是“被允許”,而并不是 和對象一樣,沒有引用了就必然會回收。關(guān)于是否要對類型進(jìn)行回收,HotSpot虛擬機(jī)提供了命令參數(shù)進(jìn)行控制,
-Xnoclassgc :關(guān)閉虛擬機(jī)對class的垃圾回收功能。
-verbose:class XXX :(XXX為程序名)你會在控制臺看到加載的類的情況。
-XX:+TraceClassLoading :監(jiān)控類的加載
-XX:+TraceClassUnLoading : 監(jiān)控類的卸載
其中-verbose:class和-XX:+TraceClassLoading可以在 Product版的虛擬機(jī)中使用,-XX:+TraceClassUnLoading參數(shù)需要FastDebug版的虛擬機(jī)支持。
在大量使用反射、動態(tài)代理、CGLib等字節(jié)碼框架,動態(tài)生成JSP以及OSGi這類頻繁自定義類加載 器的場景中,通常都需要Java虛擬機(jī)具備類型卸載的能力,以保證不會對方法區(qū)造成過大的內(nèi)存壓 力。
三、垃圾收集算法
Java默認(rèn)的虛擬機(jī)HotSpot VM,采用的追蹤式垃圾收集,也就是剛剛所說的可達(dá)分析,所以本節(jié)介紹的所有算法均屬于追蹤式垃圾收集的范疇。
1.分代收集理論
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集器,大多數(shù)都遵循了“分代收集”的理論進(jìn) 行設(shè)計,分代收集名為理論,實質(zhì)是一套符合大多數(shù)程序運行實際情況的經(jīng)驗法則,它建立在兩個分代假說之上:
1)弱分代假說(Weak Generational Hypothesis):絕大多數(shù)對象都是朝生夕滅的。
2)強(qiáng)分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消 亡
這兩個分代假說共同奠定了多款常用的垃圾收集器的一致的設(shè)計原則:收集器應(yīng)該將Java堆劃分 出不同的區(qū)域,然后將回收對象依據(jù)其年齡(年齡即對象熬過垃圾收集過程的次數(shù))分配到不同的區(qū) 域之中存儲。
在Java堆劃分出不同的區(qū)域之后,垃圾收集器才可以每次只回收其中某一個或者某些部分的區(qū)域 ——因而才有了“Minor GC”“Major GC”“Full GC”這樣的回收類型的劃分;發(fā)展出了“標(biāo)記-復(fù)制算法”“標(biāo)記-清除算 法”“標(biāo)記-整理算法”等針對性的垃圾收集算法,這一切的出現(xiàn)都始于分代收集理論。
把分代收集理論具體放到現(xiàn)在的商用Java虛擬機(jī)里,設(shè)計者一般至少會把Java堆劃分為新生代 (Young Generation)和老年代(Old Generation)兩個區(qū)域。顧名思義,在新生代中,每次垃圾收集 時都發(fā)現(xiàn)有大批對象死去,而每次回收后存活的少量對象,將會逐步晉升到老年代中存放。分代收集并非只是簡單劃分一下內(nèi)存區(qū)域那么容易,它至少存在一個明顯的困難:對象不 是孤立的,對象之間會存在跨代引用。
假如要現(xiàn)在進(jìn)行一次只局限于新生代區(qū)域內(nèi)的收集(Minor GC),但新生代中的對象是完全有可 能被老年代所引用的,為了找出該區(qū)域中的存活對象,不得不在固定的GC Roots之外,再額外遍歷整 個老年代中所有對象來確??蛇_(dá)性分析結(jié)果的正確性,反過來也是一樣。遍歷整個老年代所有對象 的方案雖然理論上可行,但無疑會為內(nèi)存回收帶來很大的性能負(fù)擔(dān)。為了解決這個問題,就需要對分 代收集理論添加第三條經(jīng)驗法則:
3)跨代引用假說(Intergenerational Reference Hypothesis):跨代引用相對于同代引用來說僅占極 少數(shù)。
依據(jù)這條假說,我們就不應(yīng)再為了少量的跨代引用去掃描整個老年代,也不必浪費空間專門記錄 每一個對象是否存在及存在哪些跨代引用,只需在新生代上建立一個全局的數(shù)據(jù)結(jié)構(gòu)(該結(jié)構(gòu)被稱 為“記憶集”,Remembered Set),這個結(jié)構(gòu)把老年代劃分成若干小塊,標(biāo)識出老年代的哪一塊內(nèi)存會 存在跨代引用。此后當(dāng)發(fā)生Minor GC時,只有包含了跨代引用的小塊內(nèi)存里的對象才會被加入到GC Roots進(jìn)行掃描。雖然這種方法需要在對象改變引用關(guān)系(如將自己或者某個屬性賦值)時維護(hù)記錄數(shù) 據(jù)的正確性,會增加一些運行時的開銷,但比起收集時掃描整個老年代來說仍然是劃算的。
2.名詞解釋
部分收集(Partial GC):指目標(biāo)不是完整收集整個Java堆的垃圾收集,其中又分為:
- 新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目標(biāo)只是老年代的垃圾收集。目前只有CMS收集器會有單 獨收集老年代的行為。另外請注意“Major GC”這個說法現(xiàn)在有點混淆,在不同資料上常有不同所指,有的是指老年代的收集、有的是指整堆收集。
- 混合收集(Mixed GC):指目標(biāo)是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收 集器會有這種行為。
- 整堆收集(Full GC):收集整個Java堆和方法區(qū)的垃圾收集。
通常能單獨發(fā)生收集行為的只是新生代,所以這里“反過來”的情況只是理論上允許,實際上除了 CMS收集器,其他都不存在只針對老年代的收集。
3.標(biāo)記-清除算法
算法分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出需要回 收的對象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對象,也可以反過來,標(biāo)記存活的對象,統(tǒng)一回 收所有未被標(biāo)記的對象。標(biāo)記過程就是對象是否屬于垃圾的判定過程,怎么判斷是否垃圾,就是剛剛所提到的,引用計數(shù)算法和可達(dá)分析算法。
它的主要缺點有兩個:第一個是執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對 象,而且其中大部分是需要被回收的,這時必須進(jìn)行大量標(biāo)記和清除的動作,導(dǎo)致標(biāo)記和清除兩個過 程的執(zhí)行效率都隨對象數(shù)量增長而降低;第二個是內(nèi)存空間的碎片化問題,標(biāo)記、清除之后會產(chǎn)生大 量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會導(dǎo)致當(dāng)以后在程序運行過程中需要分配較大對象時無法找 到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動作。標(biāo)記-清除算法的執(zhí)行過程如圖所示。

4.標(biāo)記-復(fù)制算法
標(biāo)記-復(fù)制算法常被簡稱為復(fù)制算法。為了 解決標(biāo)記-清除算法面對大量可回收對象時執(zhí)行效率低 的問題,1969年Fenichel提出了一種稱為“半?yún)^(qū)復(fù)制”(Semispace Copying)的垃圾收集算法,它將可用 內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著 的對象復(fù)制到另外一塊上面,復(fù)制的時候不用考慮有 空間碎片的復(fù)雜情況,只要移動堆頂指針,按順序分配即可,然后再把已使用過的內(nèi)存空間一次清理掉。
缺點:
如果內(nèi)存中多數(shù)對象都是存 活的,這種算法將會產(chǎn)生大量的內(nèi)存間復(fù)制的開銷。
回收算法的代價是將可用內(nèi)存縮小為了原來的一半,空間浪費未免太多了一 點。

在1989年,Andrew Appel針對具備“朝生夕滅”特點的對象,提出了一種更優(yōu)化的半?yún)^(qū)復(fù)制分代策 略,現(xiàn)在稱為“Appel式回收”。HotSpot虛擬機(jī)的Serial、ParNew等新生代收集器均采用了這種策略來設(shè) 計新生代的內(nèi)存布局。
Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的 Survivor空間,每次分配內(nèi)存只使用Eden和其中一塊Survivor。發(fā)生垃圾搜集時,將Eden和Survivor中仍 然存活的對象一次性復(fù)制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過的那塊Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內(nèi)存空間為整個新 生代容量的90%(Eden的80%加上一個Survivor的10%),只有一個Survivor空間,即10%的新生代是會 被“浪費”的。當(dāng)然,98%的對象可被回收僅僅是“普通場景”下測得的數(shù)據(jù),任何人都沒有辦法百分百 保證每次回收都只有不多于10%的對象存活,因此Appel式回收還有一個充當(dāng)罕見情況的“逃生門”的安 全設(shè)計,當(dāng)Survivor空間不足以容納一次Minor GC之后存活的對象時,就需要依賴其他內(nèi)存區(qū)域(實 際上大多就是老年代)進(jìn)行分配擔(dān)保。
內(nèi)存的分配擔(dān)保好比我們?nèi)ャy行借款,如果我們信譽(yù)很好,在98%的情況下都能按時償還,于是 銀行可能會默認(rèn)我們下一次也能按時按量地償還貸款,只需要有一個擔(dān)保人能保證如果我不能還款 時,可以從他的賬戶扣錢,那銀行就認(rèn)為沒有什么風(fēng)險了。內(nèi)存的分配擔(dān)保也一樣,如果另外一塊 Survivor空間沒有足夠空間存放上一次新生代收集下來的存活對象,這些對象便將通過分配擔(dān)保機(jī)制直 接進(jìn)入老年代,這對虛擬機(jī)來說就是安全的。
5.標(biāo)記-整理算法
針對老年代對象的存亡特征,1974年Edward Lueders提出了另外一種有針對性的“標(biāo)記-整 理”(Mark-Compact)算法,其中的標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可 回收對象進(jìn)行清理,而是讓所有存活的對象都向內(nèi)存空間一端移動,然后直接清理掉邊界以外的內(nèi) 存,“標(biāo)記-整理”算法的示意圖如圖所示。
標(biāo)記-清除算法與標(biāo)記-整理算法的本質(zhì)差異在于前者是一種非移動式的回收算法,而后者是移動 式的。是否移動回收后的存活對象是一項優(yōu)缺點并存的風(fēng)險決策:

移動則內(nèi)存回收時會更復(fù)雜,不移動則內(nèi)存分配時會 更復(fù)雜。移動雖然復(fù)雜點,但是不影響服務(wù)器的內(nèi)存吞吐量。HotSpot虛擬機(jī)里面關(guān)注吞吐量的Parallel Scavenge收集器是基于標(biāo)記-整理算法的。
還有一種“和稀泥式”解決方案可以不在內(nèi)存分配和訪問上增加太大額外負(fù)擔(dān),做法是讓虛 擬機(jī)平時多數(shù)時間都采用標(biāo)記-清除算法,暫時容忍內(nèi)存碎片的存在,直到內(nèi)存空間的碎片化程度已經(jīng) 大到影響對象分配時,再采用標(biāo)記-整理算法收集一次,以獲得規(guī)整的內(nèi)存空間。前面提到的基于標(biāo) 記-清除算法的CMS收集器面臨空間碎片過多時采用的就是這種處理辦法(CMS是老年代垃圾收集器)。
以上就是Java垃圾回收機(jī)制的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Java垃圾回收機(jī)制的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java 使用poi 導(dǎo)入Excel數(shù)據(jù)到數(shù)據(jù)庫的步驟
這篇文章主要介紹了java 使用poi 導(dǎo)入Excel 數(shù)據(jù)到數(shù)據(jù)庫的步驟,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-12-12
關(guān)于Spring3 + Mybatis3整合時多數(shù)據(jù)源動態(tài)切換的問題
這篇文章主要介紹了關(guān)于Spring3 + Mybatis3整合時多數(shù)據(jù)源動態(tài)切換的問題,需要的朋友可以參考下2017-04-04

