Java垃圾回收機(jī)制的示例詳解
一、概述
說(shuō)起垃圾收集(Garbage Collection,下文簡(jiǎn)稱(chēng)GC),有不少人把這項(xiàng)技術(shù)當(dāng)作Java語(yǔ)言的伴生產(chǎn) 物。事實(shí)上,垃圾收集的歷史遠(yuǎn)遠(yuǎn)比Java久遠(yuǎn),在1960年誕生于麻省理工學(xué)院的Lisp是第一門(mén)開(kāi)始使 用內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)的語(yǔ)言。當(dāng)Lisp還在胚胎時(shí)期時(shí),其作者John McCarthy就思考過(guò)垃圾 收集需要完成的三件事情:
- 哪些內(nèi)存需要回收?
- 什么時(shí)候回收?
- 如何回收?
經(jīng)過(guò)半個(gè)世紀(jì)的發(fā)展,今天的內(nèi)存動(dòng)態(tài)分配與內(nèi)存回收技術(shù)已經(jīng)相當(dāng)成熟,一切看起來(lái)都進(jìn)入 了“自動(dòng)化”時(shí)代,那為什么我們還要去了解垃圾收集和內(nèi)存分配?
答案很簡(jiǎn)單:當(dāng)需要排查各種內(nèi)存 溢出、內(nèi)存泄漏問(wèn)題時(shí),當(dāng)垃圾收集成為系統(tǒng)達(dá)到更高并發(fā)量的瓶頸時(shí),我們就必須對(duì)這些“自動(dòng) 化”的技術(shù)實(shí)施必要的監(jiān)控和調(diào)節(jié)。
Java內(nèi)存運(yùn) 行時(shí)區(qū)域其中程序計(jì)數(shù)器、虛擬機(jī)棧、本地方法棧3個(gè)區(qū)域隨線程而生,隨線程而滅,棧 中的棧幀隨著方法的進(jìn)入和退出而有條不紊地執(zhí)行著出棧和入棧操作。每一個(gè)棧幀中分配多少內(nèi)存基 本上是在類(lèi)結(jié)構(gòu)確定下來(lái)時(shí)就已知的,因此這幾個(gè)區(qū)域的內(nèi)存分配和回收都具備確定性, 在這幾個(gè)區(qū)域內(nèi)就不需要過(guò)多考慮如何回收的問(wèn)題,當(dāng)方法結(jié)束或者線程結(jié)束時(shí),內(nèi)存自然就跟隨著 回收了。
而Java堆和方法區(qū)這兩個(gè)區(qū)域則有著很顯著的不確定性:一個(gè)接口的多個(gè)實(shí)現(xiàn)類(lèi)需要的內(nèi)存可能 會(huì)不一樣,一個(gè)方法所執(zhí)行的不同條件分支所需要的內(nèi)存也可能不一樣,只有處于運(yùn)行期間,我們才 能知道程序究竟會(huì)創(chuàng)建哪些對(duì)象,創(chuàng)建多少個(gè)對(duì)象,這部分內(nèi)存的分配和回收是動(dòng)態(tài)的。垃圾收集器 所關(guān)注的正是這部分內(nèi)存該如何管理,本文后續(xù)討論中的“內(nèi)存”分配與回收也僅僅特指這一部分內(nèi)存。
二、對(duì)象已死?
在堆里面存放著Java世界中幾乎所有的對(duì)象實(shí)例,垃圾收集器在對(duì)堆進(jìn)行回收前,第一件事情就 是要確定這些對(duì)象之中哪些還“存活”著,哪些已經(jīng)“死去”(“死去”即不可能再被任何途徑使用的對(duì) 象)了。
這個(gè)面試的時(shí)候也經(jīng)常會(huì)問(wèn)到,依靠?jī)蓚€(gè)算法來(lái)判斷對(duì)象是否存活:
1、引用計(jì)數(shù)算法
2、可達(dá)分析算法。
1.引用計(jì)數(shù)算法
判斷對(duì)象是否存活的算法是這樣的:在對(duì)象中添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方 引用它時(shí),計(jì)數(shù)器值就加一;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減一;任何時(shí)刻計(jì)數(shù)器為零的對(duì)象就是不可 能再被使用的。
客觀地說(shuō),引用計(jì)數(shù)算法(Reference Counting)雖然占用了一些額外的內(nèi)存空間來(lái)進(jìn)行計(jì)數(shù),但 它的原理簡(jiǎn)單,判定效率也很高,很多領(lǐng)域都在用,但是,在Java 領(lǐng)域,至少主流的Java虛擬機(jī)里面都沒(méi)有選用引用計(jì)數(shù)算法來(lái)管理內(nèi)存。原因:?jiǎn)渭兊囊糜?jì)數(shù) 就很難解決對(duì)象之間相互循環(huán)引用的問(wèn)題。
代碼示例:
在下面testGC()方法:對(duì)象objA和objB都有字段instance,賦值令 objA.instance=objB及objB.instance=objA,除此之外,這兩個(gè)對(duì)象再無(wú)任何引用,實(shí)際上這兩個(gè)對(duì)象已 經(jīng)不可能再被訪問(wèn),但是它們因?yàn)榛ハ嘁弥鴮?duì)方,導(dǎo)致它們的引用計(jì)數(shù)都不為零,引用計(jì)數(shù)算法也 就無(wú)法回收它們。
對(duì)于objA = null,objB = null應(yīng)該有的人會(huì)不理解為什么要這么做,咱們要測(cè)試的是屬性出現(xiàn)了互相引用,是否會(huì)被gc掉。那么我就得保證其他地方不再引用他,我們用的是main方法測(cè)試,所以需要設(shè)置為null。正常在java應(yīng)用是不需要的,線程執(zhí)行過(guò)后,局部變量將被銷(xiāo)毀。那也就不存在引用這一說(shuō)了。
雖然手動(dòng)的讓局部變量不在引用堆中的對(duì)象,但是在堆內(nèi)存當(dāng)中這兩個(gè)對(duì)象的屬性還是相互引用的。如果按照計(jì)數(shù)算法,他是不應(yīng)該被gc掉的。
public class ReferenceCountingGC { public Object instance = null; private static final int _1MB = 1024 * 1024; /*** 這個(gè)成員屬性的唯一意義就是占點(diǎn)內(nèi)存,以便能在GC日志中看清楚是否有回收過(guò) */ 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è)置啟動(dòng)參數(shù):輸出GC的詳細(xì)日志
-XX:+PrintGCDetails
運(yùn)行結(jié)果:
從運(yùn)行結(jié)果中可以清楚看到內(nèi)存回收日志中包含“9257K->823K”,意味著虛擬機(jī)并沒(méi)有因?yàn)檫@兩 個(gè)對(duì)象互相引用就放棄回收它們,這也從側(cè)面說(shuō)明了Java虛擬機(jī)并不是通過(guò)引用計(jì)數(shù)算法來(lái)判斷對(duì)象 是否存活的。
2.可達(dá)性分析算法
當(dāng)前主流的商用程序語(yǔ)言(Java、C#,上溯至前面提到的古老的Lisp)的內(nèi)存管理子系統(tǒng),都是 通過(guò)可達(dá)性分析(Reachability Analysis)算法來(lái)判定對(duì)象是否存活的。這個(gè)算法的基本思路就是通過(guò) 一系列稱(chēng)為“GC Roots”的根對(duì)象作為起始節(jié)點(diǎn)集,從這些節(jié)點(diǎn)開(kāi)始,根據(jù)引用關(guān)系向下搜索,搜索過(guò) 程所走過(guò)的路徑稱(chēng)為“引用鏈”(Reference Chain),如果某個(gè)對(duì)象到GC Roots間沒(méi)有任何引用鏈相連, 或者用圖論的話來(lái)說(shuō)就是從GC Roots到這個(gè)對(duì)象不可達(dá)時(shí),則證明此對(duì)象是不可能再被使用的。
如圖所示,對(duì)象object 5、object 6、object 7雖然互有關(guān)聯(lián),但是它們到GC Roots是不可達(dá)的, 因此它們將會(huì)被判定為可回收的對(duì)象。
在Java技術(shù)體系里面,固定可作為GC Roots的對(duì)象包括以下幾種:
- 在虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象,譬如各個(gè)線程被調(diào)用的方法堆棧中使用到的 參數(shù)、局部變量、臨時(shí)變量等。
- 在方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象,譬如Java類(lèi)的引用類(lèi)型靜態(tài)變量。
- 在方法區(qū)中常量引用的對(duì)象,譬如字符串常量池(String Table)里的引用。
- 在本地方法棧中JNI(即通常所說(shuō)的Native方法)引用的對(duì)象。
- Java虛擬機(jī)內(nèi)部的引用,如基本數(shù)據(jù)類(lèi)型對(duì)應(yīng)的Class對(duì)象,一些常駐的異常對(duì)象(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統(tǒng)類(lèi)加載器。
- 所有被同步鎖(synchronized關(guān)鍵字)持有的對(duì)象。
- 反映Java虛擬機(jī)內(nèi)部情況的JMXBean、JVMTI中注冊(cè)的回調(diào)、本地代碼緩存等。
- 除了這些固定的GC Roots集合以外,根據(jù)用戶(hù)所選用的垃圾收集器以及當(dāng)前回收的內(nèi)存區(qū)域不 同,還可以有其他對(duì)象“臨時(shí)性”地加入,共同構(gòu)成完整GC Roots集合。譬如后文將會(huì)提到的分代收集 和局部回收(Partial GC),如果只針對(duì)Java堆中某一塊區(qū)域發(fā)起垃圾收集時(shí)(如最典型的只針對(duì)新生 代的垃圾收集),必須考慮到內(nèi)存區(qū)域是虛擬機(jī)自己的實(shí)現(xiàn)細(xì)節(jié)(在用戶(hù)視角里任何內(nèi)存區(qū)域都是不 可見(jiàn)的),更不是孤立封閉的,所以某個(gè)區(qū)域里的對(duì)象完全有可能被位于堆中其他區(qū)域的對(duì)象所引 用,這時(shí)候就需要將這些關(guān)聯(lián)區(qū)域的對(duì)象也一并加入GC Roots集合中去,才能保證可達(dá)性分析的正確 性。
目前最新的幾款垃圾收集器無(wú)一例外都具備了局部回收的特征。如OpenJDK中的G1、Shenandoah、ZGC以及Azul的PGC、C4這些收集器。
3.四種引用
無(wú)論是通過(guò)引用計(jì)數(shù)算法判斷對(duì)象的引用數(shù)量,還是通過(guò)可達(dá)性分析算法判斷對(duì)象是否引用鏈可 達(dá),判定對(duì)象是否存活都和“引用”離不開(kāi)關(guān)系。在JDK 1.2版之前,Java里面的引用是很傳統(tǒng)的定義: 如果reference類(lèi)型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱(chēng)該reference數(shù)據(jù)是代表 某塊內(nèi)存、某個(gè)對(duì)象的引用。一個(gè)對(duì)象在 這種定義下只有“被引用”或者“未被引用”兩種狀態(tài),對(duì)于描述一些“食之無(wú)味,棄之可惜”的對(duì)象就顯 得無(wú)能為力。譬如我們希望能描述一類(lèi)對(duì)象:當(dāng)內(nèi)存空間還足夠時(shí),能保留在內(nèi)存之中,如果內(nèi)存空 間在進(jìn)行垃圾收集后仍然非常緊張,那就可以拋棄這些對(duì)象——很多系統(tǒng)的緩存功能都符合這樣的應(yīng) 用場(chǎng)景。
在JDK 1.2版之后,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strongly Re-ference)、軟 引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference)4種,這4種引用強(qiáng) 度依次逐漸減弱。
- 強(qiáng)引用是最傳統(tǒng)的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類(lèi)似“Object obj=new Object()”這種引用關(guān)系。無(wú)論任何情況下,只要強(qiáng)引用關(guān)系還存在,垃圾收集器就永遠(yuǎn)不會(huì)回 收掉被引用的對(duì)象。
- 軟引用是用來(lái)描述一些還有用,但非必須的對(duì)象。只被軟引用關(guān)聯(lián)著的對(duì)象,在系統(tǒng)將要發(fā)生內(nèi) 存溢出異常前,會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒(méi)有足夠的內(nèi)存, 才會(huì)拋出內(nèi)存溢出異常。在JDK 1.2版之后提供了SoftReference類(lèi)來(lái)實(shí)現(xiàn)軟引用。
- 弱引用也是用來(lái)描述那些非必須對(duì)象,但是它的強(qiáng)度比軟引用更弱一些,被弱引用關(guān)聯(lián)的對(duì)象只 能生存到下一次垃圾收集發(fā)生為止。當(dāng)垃圾收集器開(kāi)始工作,無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只 被弱引用關(guān)聯(lián)的對(duì)象。在JDK 1.2版之后提供了WeakReference類(lèi)來(lái)實(shí)現(xiàn)弱引用。
- 虛引用也稱(chēng)為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關(guān)系。一個(gè)對(duì)象是否有虛引用的 存在,完全不會(huì)對(duì)其生存時(shí)間構(gòu)成影響,也無(wú)法通過(guò)虛引用來(lái)取得一個(gè)對(duì)象實(shí)例。為一個(gè)對(duì)象設(shè)置虛 引用關(guān)聯(lián)的唯一目的只是為了能在這個(gè)對(duì)象被收集器回收時(shí)收到一個(gè)系統(tǒng)通知。在JDK 1.2版之后提供 了PhantomReference類(lèi)來(lái)實(shí)現(xiàn)虛引用。
4.生存還是死亡?
即使在可達(dá)性分析算法中判定為不可達(dá)的對(duì)象,也不是“非死不可”的,這時(shí)候它們暫時(shí)還處于“緩 刑”階段,要真正宣告一個(gè)對(duì)象死亡,至少要經(jīng)歷兩次標(biāo)記過(guò)程:如果對(duì)象在進(jìn)行可達(dá)性分析后發(fā)現(xiàn)沒(méi) 有與GC Roots相連接的引用鏈,那它將會(huì)被第一次標(biāo)記,隨后進(jìn)行一次篩選,篩選的條件是此對(duì)象是 否有必要執(zhí)行finalize()方法。假如對(duì)象沒(méi)有覆蓋finalize()方法,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用 過(guò),那么虛擬機(jī)將這兩種情況都視為“沒(méi)有必要執(zhí)行”。
finalize()方法是Object的一個(gè)方法,所有的對(duì)象都是Object的子類(lèi)。只不過(guò)語(yǔ)法上默認(rèn)沒(méi)有繼承Object。但是實(shí)際上是繼承的Object類(lèi)。
如果這個(gè)對(duì)象被判定為確有必要執(zhí)行finalize()方法,那么該對(duì)象將會(huì)被放置在一個(gè)名為F-Queue的 隊(duì)列之中,并在稍后由一條由虛擬機(jī)自動(dòng)建立的、低調(diào)度優(yōu)先級(jí)的Finalizer線程去執(zhí)行它們的finalize() 方法。這里所說(shuō)的“執(zhí)行”是指虛擬機(jī)會(huì)觸發(fā)這個(gè)方法開(kāi)始運(yùn)行,但并不承諾一定會(huì)等待它運(yùn)行結(jié)束。
這樣做的原因是,如果某個(gè)對(duì)象的finalize()方法執(zhí)行緩慢,或者更極端地發(fā)生了死循環(huán),將很可能導(dǎo) 致F-Queue隊(duì)列中的其他對(duì)象永久處于等待。
如果對(duì) 象要在finalize()中成功拯救自己 只要重新與引用鏈上的任何一個(gè)對(duì)象建立關(guān)聯(lián)即可,譬如把自己 (this關(guān)鍵字)賦值給某個(gè)類(lèi)變量或者對(duì)象的成員變量,那在第二次標(biāo)記時(shí)它將被移出“即將回收”的集 合;如果對(duì)象這時(shí)候還沒(méi)有逃脫,那基本上它就真的要被回收了。從下面代碼我們可以看到一個(gè) 對(duì)象的finalize()被執(zhí)行,但是它仍然可以存活。
一次對(duì)象自我拯救的演示:
此代碼演示了兩點(diǎn):
1.對(duì)象可以在被GC時(shí)自我拯救。
2.這種自救的機(jī)會(huì)只有一次,因?yàn)橐粋€(gè)對(duì)象的finalize()方法最多只會(huì)被系統(tǒng)自動(dò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(); //對(duì)象第一次成功拯救自己 SAVE_HOOK = null; System.gc(); // 因?yàn)镕inalizer方法優(yōu)先級(jí)很低,暫停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(); // 因?yàn)镕inalizer方法優(yōu)先級(jí)很低,暫停0.5秒,以等待它 Thread.sleep(500); if (SAVE_HOOK != null) { SAVE_HOOK.isAlive(); } else { System.out.println("no, i am dead :("); } } }
運(yùn)行結(jié)果:
finalize()方法大家盡量避免使用它,它的運(yùn)行代價(jià)高昂,不確定性大,無(wú)法保證各個(gè)對(duì)象的調(diào)用順序,如今已被官方明確聲明為 不推薦使用的語(yǔ)法。finalize()能做的所有工作,使用try-finally或者其他方式都可以做得更好、 更及時(shí)。
5.回收方法區(qū)
方法區(qū)的垃圾收集主要回收兩部分內(nèi)容:廢棄的常量和不再使用的類(lèi)型?;厥諒U棄常量與回收 Java堆中的對(duì)象非常類(lèi)似。舉個(gè)常量池中字面量回收的例子,假如一個(gè)字符串“java”曾經(jīng)進(jìn)入常量池 中,但是當(dāng)前系統(tǒng)又沒(méi)有任何一個(gè)字符串對(duì)象的值是“java”,換句話說(shuō),已經(jīng)沒(méi)有任何字符串對(duì)象引用 常量池中的“java”常量,且虛擬機(jī)中也沒(méi)有其他地方引用這個(gè)字面量。如果在這時(shí)發(fā)生內(nèi)存回收,這個(gè)“java”常量就將會(huì)被系統(tǒng)清理出常量池。常量池中其他類(lèi)(接 口)、方法、字段的符號(hào)引用也與此類(lèi)似。
判定一個(gè)常量是否“廢棄”還是相對(duì)簡(jiǎn)單,而要判定一個(gè)類(lèi)型是否屬于“不再被使用的類(lèi)”的條件就 比較苛刻了。需要同時(shí)滿(mǎn)足下面三個(gè)條件:
- 該類(lèi)所有的實(shí)例都已經(jīng)被回收,也就是Java堆中不存在該類(lèi)及其任何派生子類(lèi)的實(shí)例。
- 加載該類(lèi)的類(lèi)加載器已經(jīng)被回收,這個(gè)條件除非是經(jīng)過(guò)精心設(shè)計(jì)的可替換類(lèi)加載器的場(chǎng)景,如 OSGi、JSP的重加載等,否則通常是很難達(dá)成的。
- 該類(lèi)對(duì)應(yīng)的java.lang.Class對(duì)象沒(méi)有在任何地方被引用,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類(lèi)的方 法。
- Java虛擬機(jī)被允許對(duì)滿(mǎn)足上述三個(gè)條件的無(wú)用類(lèi)進(jìn)行回收,這里說(shuō)的僅僅是“被允許”,而并不是 和對(duì)象一樣,沒(méi)有引用了就必然會(huì)回收。關(guān)于是否要對(duì)類(lèi)型進(jìn)行回收,HotSpot虛擬機(jī)提供了命令參數(shù)進(jìn)行控制,
-Xnoclassgc :關(guān)閉虛擬機(jī)對(duì)class的垃圾回收功能。
-verbose:class XXX :(XXX為程序名)你會(huì)在控制臺(tái)看到加載的類(lèi)的情況。
-XX:+TraceClassLoading :監(jiān)控類(lèi)的加載
-XX:+TraceClassUnLoading : 監(jiān)控類(lèi)的卸載
其中-verbose:class和-XX:+TraceClassLoading可以在 Product版的虛擬機(jī)中使用,-XX:+TraceClassUnLoading參數(shù)需要FastDebug版的虛擬機(jī)支持。
在大量使用反射、動(dòng)態(tài)代理、CGLib等字節(jié)碼框架,動(dòng)態(tài)生成JSP以及OSGi這類(lèi)頻繁自定義類(lèi)加載 器的場(chǎng)景中,通常都需要Java虛擬機(jī)具備類(lèi)型卸載的能力,以保證不會(huì)對(duì)方法區(qū)造成過(guò)大的內(nèi)存壓 力。
三、垃圾收集算法
Java默認(rèn)的虛擬機(jī)HotSpot VM,采用的追蹤式垃圾收集,也就是剛剛所說(shuō)的可達(dá)分析,所以本節(jié)介紹的所有算法均屬于追蹤式垃圾收集的范疇。
1.分代收集理論
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集器,大多數(shù)都遵循了“分代收集”的理論進(jìn) 行設(shè)計(jì),分代收集名為理論,實(shí)質(zhì)是一套符合大多數(shù)程序運(yùn)行實(shí)際情況的經(jīng)驗(yàn)法則,它建立在兩個(gè)分代假說(shuō)之上:
1)弱分代假說(shuō)(Weak Generational Hypothesis):絕大多數(shù)對(duì)象都是朝生夕滅的。
2)強(qiáng)分代假說(shuō)(Strong Generational Hypothesis):熬過(guò)越多次垃圾收集過(guò)程的對(duì)象就越難以消 亡
這兩個(gè)分代假說(shuō)共同奠定了多款常用的垃圾收集器的一致的設(shè)計(jì)原則:收集器應(yīng)該將Java堆劃分 出不同的區(qū)域,然后將回收對(duì)象依據(jù)其年齡(年齡即對(duì)象熬過(guò)垃圾收集過(guò)程的次數(shù))分配到不同的區(qū) 域之中存儲(chǔ)。
在Java堆劃分出不同的區(qū)域之后,垃圾收集器才可以每次只回收其中某一個(gè)或者某些部分的區(qū)域 ——因而才有了“Minor GC”“Major GC”“Full GC”這樣的回收類(lèi)型的劃分;發(fā)展出了“標(biāo)記-復(fù)制算法”“標(biāo)記-清除算 法”“標(biāo)記-整理算法”等針對(duì)性的垃圾收集算法,這一切的出現(xiàn)都始于分代收集理論。
把分代收集理論具體放到現(xiàn)在的商用Java虛擬機(jī)里,設(shè)計(jì)者一般至少會(huì)把Java堆劃分為新生代 (Young Generation)和老年代(Old Generation)兩個(gè)區(qū)域。顧名思義,在新生代中,每次垃圾收集 時(shí)都發(fā)現(xiàn)有大批對(duì)象死去,而每次回收后存活的少量對(duì)象,將會(huì)逐步晉升到老年代中存放。分代收集并非只是簡(jiǎn)單劃分一下內(nèi)存區(qū)域那么容易,它至少存在一個(gè)明顯的困難:對(duì)象不 是孤立的,對(duì)象之間會(huì)存在跨代引用。
假如要現(xiàn)在進(jìn)行一次只局限于新生代區(qū)域內(nèi)的收集(Minor GC),但新生代中的對(duì)象是完全有可 能被老年代所引用的,為了找出該區(qū)域中的存活對(duì)象,不得不在固定的GC Roots之外,再額外遍歷整 個(gè)老年代中所有對(duì)象來(lái)確保可達(dá)性分析結(jié)果的正確性,反過(guò)來(lái)也是一樣。遍歷整個(gè)老年代所有對(duì)象 的方案雖然理論上可行,但無(wú)疑會(huì)為內(nèi)存回收帶來(lái)很大的性能負(fù)擔(dān)。為了解決這個(gè)問(wèn)題,就需要對(duì)分 代收集理論添加第三條經(jīng)驗(yàn)法則:
3)跨代引用假說(shuō)(Intergenerational Reference Hypothesis):跨代引用相對(duì)于同代引用來(lái)說(shuō)僅占極 少數(shù)。
依據(jù)這條假說(shuō),我們就不應(yīng)再為了少量的跨代引用去掃描整個(gè)老年代,也不必浪費(fèi)空間專(zhuān)門(mén)記錄 每一個(gè)對(duì)象是否存在及存在哪些跨代引用,只需在新生代上建立一個(gè)全局的數(shù)據(jù)結(jié)構(gòu)(該結(jié)構(gòu)被稱(chēng) 為“記憶集”,Remembered Set),這個(gè)結(jié)構(gòu)把老年代劃分成若干小塊,標(biāo)識(shí)出老年代的哪一塊內(nèi)存會(huì) 存在跨代引用。此后當(dāng)發(fā)生Minor GC時(shí),只有包含了跨代引用的小塊內(nèi)存里的對(duì)象才會(huì)被加入到GC Roots進(jìn)行掃描。雖然這種方法需要在對(duì)象改變引用關(guān)系(如將自己或者某個(gè)屬性賦值)時(shí)維護(hù)記錄數(shù) 據(jù)的正確性,會(huì)增加一些運(yùn)行時(shí)的開(kāi)銷(xiāo),但比起收集時(shí)掃描整個(gè)老年代來(lái)說(shuō)仍然是劃算的。
2.名詞解釋
部分收集(Partial GC):指目標(biāo)不是完整收集整個(gè)Java堆的垃圾收集,其中又分為:
- 新生代收集(Minor GC/Young GC):指目標(biāo)只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目標(biāo)只是老年代的垃圾收集。目前只有CMS收集器會(huì)有單 獨(dú)收集老年代的行為。另外請(qǐng)注意“Major GC”這個(gè)說(shuō)法現(xiàn)在有點(diǎn)混淆,在不同資料上常有不同所指,有的是指老年代的收集、有的是指整堆收集。
- 混合收集(Mixed GC):指目標(biāo)是收集整個(gè)新生代以及部分老年代的垃圾收集。目前只有G1收 集器會(huì)有這種行為。
- 整堆收集(Full GC):收集整個(gè)Java堆和方法區(qū)的垃圾收集。
通常能單獨(dú)發(fā)生收集行為的只是新生代,所以這里“反過(guò)來(lái)”的情況只是理論上允許,實(shí)際上除了 CMS收集器,其他都不存在只針對(duì)老年代的收集。
3.標(biāo)記-清除算法
算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出需要回 收的對(duì)象,在標(biāo)記完成后,統(tǒng)一回收掉所有被標(biāo)記的對(duì)象,也可以反過(guò)來(lái),標(biāo)記存活的對(duì)象,統(tǒng)一回 收所有未被標(biāo)記的對(duì)象。標(biāo)記過(guò)程就是對(duì)象是否屬于垃圾的判定過(guò)程,怎么判斷是否垃圾,就是剛剛所提到的,引用計(jì)數(shù)算法和可達(dá)分析算法。
它的主要缺點(diǎn)有兩個(gè):第一個(gè)是執(zhí)行效率不穩(wěn)定,如果Java堆中包含大量對(duì) 象,而且其中大部分是需要被回收的,這時(shí)必須進(jìn)行大量標(biāo)記和清除的動(dòng)作,導(dǎo)致標(biāo)記和清除兩個(gè)過(guò) 程的執(zhí)行效率都隨對(duì)象數(shù)量增長(zhǎng)而降低;第二個(gè)是內(nèi)存空間的碎片化問(wèn)題,標(biāo)記、清除之后會(huì)產(chǎn)生大 量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致當(dāng)以后在程序運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí)無(wú)法找 到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。標(biāo)記-清除算法的執(zhí)行過(guò)程如圖所示。
4.標(biāo)記-復(fù)制算法
標(biāo)記-復(fù)制算法常被簡(jiǎn)稱(chēng)為復(fù)制算法。為了 解決標(biāo)記-清除算法面對(duì)大量可回收對(duì)象時(shí)執(zhí)行效率低 的問(wèn)題,1969年Fenichel提出了一種稱(chēng)為“半?yún)^(qū)復(fù)制”(Semispace Copying)的垃圾收集算法,它將可用 內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著 的對(duì)象復(fù)制到另外一塊上面,復(fù)制的時(shí)候不用考慮有 空間碎片的復(fù)雜情況,只要移動(dòng)堆頂指針,按順序分配即可,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。
缺點(diǎn):
如果內(nèi)存中多數(shù)對(duì)象都是存 活的,這種算法將會(huì)產(chǎn)生大量的內(nèi)存間復(fù)制的開(kāi)銷(xiāo)。
回收算法的代價(jià)是將可用內(nèi)存縮小為了原來(lái)的一半,空間浪費(fèi)未免太多了一 點(diǎn)。
在1989年,Andrew Appel針對(duì)具備“朝生夕滅”特點(diǎn)的對(duì)象,提出了一種更優(yōu)化的半?yún)^(qū)復(fù)制分代策 略,現(xiàn)在稱(chēng)為“Appel式回收”。HotSpot虛擬機(jī)的Serial、ParNew等新生代收集器均采用了這種策略來(lái)設(shè) 計(jì)新生代的內(nèi)存布局。
Appel式回收的具體做法是把新生代分為一塊較大的Eden空間和兩塊較小的 Survivor空間,每次分配內(nèi)存只使用Eden和其中一塊Survivor。發(fā)生垃圾搜集時(shí),將Eden和Survivor中仍 然存活的對(duì)象一次性復(fù)制到另外一塊Survivor空間上,然后直接清理掉Eden和已用過(guò)的那塊Survivor空間。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8∶1,也即每次新生代中可用內(nèi)存空間為整個(gè)新 生代容量的90%(Eden的80%加上一個(gè)Survivor的10%),只有一個(gè)Survivor空間,即10%的新生代是會(huì) 被“浪費(fèi)”的。當(dāng)然,98%的對(duì)象可被回收僅僅是“普通場(chǎng)景”下測(cè)得的數(shù)據(jù),任何人都沒(méi)有辦法百分百 保證每次回收都只有不多于10%的對(duì)象存活,因此Appel式回收還有一個(gè)充當(dāng)罕見(jiàn)情況的“逃生門(mén)”的安 全設(shè)計(jì),當(dāng)Survivor空間不足以容納一次Minor GC之后存活的對(duì)象時(shí),就需要依賴(lài)其他內(nèi)存區(qū)域(實(shí) 際上大多就是老年代)進(jìn)行分配擔(dān)保。
內(nèi)存的分配擔(dān)保好比我們?nèi)ャy行借款,如果我們信譽(yù)很好,在98%的情況下都能按時(shí)償還,于是 銀行可能會(huì)默認(rèn)我們下一次也能按時(shí)按量地償還貸款,只需要有一個(gè)擔(dān)保人能保證如果我不能還款 時(shí),可以從他的賬戶(hù)扣錢(qián),那銀行就認(rèn)為沒(méi)有什么風(fēng)險(xiǎn)了。內(nèi)存的分配擔(dān)保也一樣,如果另外一塊 Survivor空間沒(méi)有足夠空間存放上一次新生代收集下來(lái)的存活對(duì)象,這些對(duì)象便將通過(guò)分配擔(dān)保機(jī)制直 接進(jìn)入老年代,這對(duì)虛擬機(jī)來(lái)說(shuō)就是安全的。
5.標(biāo)記-整理算法
針對(duì)老年代對(duì)象的存亡特征,1974年Edward Lueders提出了另外一種有針對(duì)性的“標(biāo)記-整 理”(Mark-Compact)算法,其中的標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對(duì)可 回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向內(nèi)存空間一端移動(dòng),然后直接清理掉邊界以外的內(nèi) 存,“標(biāo)記-整理”算法的示意圖如圖所示。
標(biāo)記-清除算法與標(biāo)記-整理算法的本質(zhì)差異在于前者是一種非移動(dòng)式的回收算法,而后者是移動(dòng) 式的。是否移動(dòng)回收后的存活對(duì)象是一項(xiàng)優(yōu)缺點(diǎn)并存的風(fēng)險(xiǎn)決策:
移動(dòng)則內(nèi)存回收時(shí)會(huì)更復(fù)雜,不移動(dòng)則內(nèi)存分配時(shí)會(huì) 更復(fù)雜。移動(dòng)雖然復(fù)雜點(diǎn),但是不影響服務(wù)器的內(nèi)存吞吐量。HotSpot虛擬機(jī)里面關(guān)注吞吐量的Parallel Scavenge收集器是基于標(biāo)記-整理算法的。
還有一種“和稀泥式”解決方案可以不在內(nèi)存分配和訪問(wèn)上增加太大額外負(fù)擔(dān),做法是讓虛 擬機(jī)平時(shí)多數(shù)時(shí)間都采用標(biāo)記-清除算法,暫時(shí)容忍內(nèi)存碎片的存在,直到內(nèi)存空間的碎片化程度已經(jīng) 大到影響對(duì)象分配時(shí),再采用標(biāo)記-整理算法收集一次,以獲得規(guī)整的內(nèi)存空間。前面提到的基于標(biāo) 記-清除算法的CMS收集器面臨空間碎片過(guò)多時(shí)采用的就是這種處理辦法(CMS是老年代垃圾收集器)。
以上就是Java垃圾回收機(jī)制的示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Java垃圾回收機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Maven搭建springboot項(xiàng)目的方法步驟
這篇文章主要介紹了Maven搭建springboot項(xiàng)目的方法步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04java 使用poi 導(dǎo)入Excel數(shù)據(jù)到數(shù)據(jù)庫(kù)的步驟
這篇文章主要介紹了java 使用poi 導(dǎo)入Excel 數(shù)據(jù)到數(shù)據(jù)庫(kù)的步驟,幫助大家更好的理解和使用Java,感興趣的朋友可以了解下2020-12-12Java 基于AQS實(shí)現(xiàn)一個(gè)同步器
這篇文章主要介紹了如何基于AQS實(shí)現(xiàn)一個(gè)同步器,幫助大家更好的理解和學(xué)習(xí)Java并發(fā),感興趣的朋友可以了解下2020-09-09SpringBoot實(shí)現(xiàn)啟動(dòng)類(lèi)的存放位置
這篇文章主要介紹了SpringBoot實(shí)現(xiàn)啟動(dòng)類(lèi)的存放位置,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01關(guān)于Spring3 + Mybatis3整合時(shí)多數(shù)據(jù)源動(dòng)態(tài)切換的問(wèn)題
這篇文章主要介紹了關(guān)于Spring3 + Mybatis3整合時(shí)多數(shù)據(jù)源動(dòng)態(tài)切換的問(wèn)題,需要的朋友可以參考下2017-04-04Java jwt使用公鑰字符串驗(yàn)證解析token鎖方法詳解
關(guān)于java獲取Token驗(yàn)證的問(wèn)題相信很多人都遇見(jiàn)過(guò),尤其是對(duì)剛接觸微信開(kāi)發(fā)的人來(lái)說(shuō)確實(shí)有點(diǎn)棘手,下面這篇文章主要給大家介紹了關(guān)于Java中token驗(yàn)證解析的相關(guān)資料,需要的朋友可以參考下2023-02-02