深入理解Java虛擬機2:垃圾收集算法

所謂垃圾收集泞辐,就是清理已經(jīng)不再使用的內(nèi)存空間硝逢,提高內(nèi)存的利用率秒啦。
由于程序計數(shù)器遭赂、虛擬機棧、本地方法棧都隨線程而生而滅梗劫,棧中的內(nèi)存空間也都基本在編譯期間就可以確定玻淑,所以不需要進行垃圾收集舰涌;而方法區(qū)和Java 堆則不一樣陵像,它們具有不確定性,只有在程序運行期間才能確定會創(chuàng)建哪些對象寇壳,這部分內(nèi)存的回收和分配都是動態(tài)的醒颖,本篇文章后續(xù)講述的“垃圾收集”就是針對這部分內(nèi)存。

談到垃圾收集壳炎,就需要問三個問題:

  • What: 哪些內(nèi)存需要回收泞歉?
  • When: 什么時候回收逼侦?
  • How: 如何回收?

讓我們從這三個基本問題開始進入本篇的學(xué)習(xí)吧腰耙!


哪些內(nèi)存需要回收榛丢?

需要回收的內(nèi)存肯定是沒有再被引用的對象實例,它們永遠(yuǎn)都無法再被繼續(xù)調(diào)用了挺庞,也就是“名存實亡”了晰赞。那么該如何知道對象是否被引用了呢?

引用計數(shù)算法

這個算法很簡單高效选侨,就是一個對象引用它的時候計數(shù)器就加1掖鱼,引用失效(例如A = null)就減1,當(dāng)一個對象的引用計數(shù)器為0時就說明沒有對象引用它了援制,那就表示它“死亡”了戏挡。
然而至少 Java 虛擬機里并沒有用引用計數(shù)算法進行內(nèi)存管理,因為它無法解決相互循環(huán)引用的問題晨仑。

public class ReferenceCountingGC {
    public Object instance = null;

    public static void testGC() {
        ReferenceCountingGC refA = new ReferenceCountingGC();
        ReferenceCountingGC refB = new ReferenceCountingGC();
        refA.instance = refB;
        refB.instance = refA;
        
        refA = null;
        refB = null;
        System.gc();
    }
}

例如上面一段代碼褐墅,兩個對象分別把持著對方對象的引用,這將導(dǎo)致它們的引用計數(shù)器都不能變?yōu)?洪己,從而也不能被回收妥凳。

可達(dá)性分析算法

在主流的商用程序設(shè)計語言中都基本上采用可達(dá)性分析算法來判斷對象是否存活的。這個算法就是把一系列的GC Root作為根節(jié)點码泛,從它們開始向下遍歷猾封,若一個對象到任何GC Root間均不可達(dá),那它就是不可用的對象噪珊,可以被回收晌缘,因此這種算法又叫做根節(jié)點枚舉

可達(dá)性分析算法判斷對象是否可回收
就像圖中的 object 5痢站、object 6磷箕、object 7雖然互相有關(guān)聯(lián),但到GC Root卻是不可達(dá)的阵难,所以會被判定為可回收對象岳枷。
在 Java 語言中,可作為GC Root的對象包括下面幾種:

  • 虛擬機棧中引用的對象(也就是局部變量引用的對象)
  • 方法區(qū)的靜態(tài)變量所引用的對象
  • 方法區(qū)中常量引用的對象
  • 本地方法棧 JNI 引用的對象

就拿上面那段代碼來說呜叫,在執(zhí)行完

refA = null;
refB = null;

以后空繁,那兩個實例對象雖然都懷著對方的引用,但均無法作為GC Root朱庆,又沒有其他GC Root與他們建立聯(lián)系盛泡,所以都會被視為可以回收的對象而被回收。

生存還是死亡

然而通過可達(dá)性分析被標(biāo)記為可以回收的對象就一定“非死不可”嗎娱颊?不一定傲诵,要真正宣告一個對象死亡凯砍,至少要經(jīng)歷兩次標(biāo)記過程:在對象被可達(dá)性分析算法標(biāo)記為可以回收后,會進行一次篩選拴竹,篩選出有必要執(zhí)行finalize方法的對象悟衩,然后把它們放置在一個低優(yōu)先級的線程隊列中依次執(zhí)行finalize方法。

那什么叫沒有必要執(zhí)行 finalize方法呢栓拜?就是當(dāng)對象沒有覆蓋默認(rèn)的finalize方法或者這個finalize方法之前被虛擬機調(diào)用過一次座泳。這兩種情況下虛擬機都會認(rèn)為沒有必要執(zhí)行finalize方法,這樣對象就真的死亡了菱属。

而在對象執(zhí)行完finalize方法之后钳榨,虛擬機會對這些對象進行第二次標(biāo)記,若它還沒有與任何GC Root建立聯(lián)系的話纽门,那就真的會被回收薛耻。也就是說,只要覆蓋對象的finalize方法赏陵,使它與任何一個局部變量或靜態(tài)變量建立引用關(guān)系饼齿,那么它就可以免除被回收的命運,從而“自救成功”蝙搔。但這種自救只能拯救一次缕溉,因為一個對象的finalize方法最多只能被執(zhí)行一次。


什么時候回收吃型?

在進行可達(dá)性分析的時候证鸥,所有線程都需要暫停下來,因為若是出現(xiàn)分析過程中對象的引用關(guān)系還在不斷變化的狀況勤晚,分析結(jié)果的準(zhǔn)確性就無法達(dá)成保障枉层。那么問題就來了,系統(tǒng)應(yīng)該在什么時候暫停所有的線程并進行 GC 呢赐写?

安全點

Java 程序執(zhí)行時并非在任何地方都能停下來開始 GC鸟蜡,只有在到達(dá)一些特定的地方才行,這些地方叫安全點(Safepoint)挺邀。 安全點的選擇既不能太少以致于長時間沒有進行 GC揉忘,又不能太頻繁以致于過分增大運行時的負(fù)荷。所以安全點的選擇基本上是以附近的語句“是否具有讓程序長時間執(zhí)行的特征”為標(biāo)準(zhǔn)進行選定的端铛,而“長時間執(zhí)行”執(zhí)行最明顯的特征就是指令序列復(fù)用泣矛,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)禾蚕、異常跳轉(zhuǎn)等地方附近都會有安全點乳蓄。

對于安全點,還有一個問題需要考慮夕膀,如何在 GC 發(fā)生時讓所有的線程都跑到最近的安全點上停下來虚倒。主要有兩種方案可供選擇:
搶先式中斷:GC 發(fā)生時,先將所有線程都暫停下來产舞,若是有些線程不在安全點魂奥,則恢復(fù)線程,讓它跑到最近的安全點再停下來易猫。這種方案幾乎沒有虛擬機采用耻煤。
主動式中斷:GC 發(fā)生時,系統(tǒng)先將一個標(biāo)志位設(shè)置為真准颓,各個線程在執(zhí)行到安全點的時候判斷這個標(biāo)志位哈蝇,若發(fā)現(xiàn)其為真就中斷掛起。直到所有線程都停下來之后再進行可達(dá)性分析等操作攘已。這個方法簡單高效炮赦,使用比較普遍。

安全區(qū)域

上面的方法似乎完美解決了這個問題样勃,但只是對于正在執(zhí)行的線程吠勘,若是那些處于睡眠或阻塞狀態(tài)的線程,它們顯然無法到達(dá)安全點峡眶,而 JVM 也不可能等待 CPU 重新調(diào)度它剧防。這個時候就需要安全區(qū)域(Safe Region)了。

安全區(qū)域是指在一段引用關(guān)系不會發(fā)生變化的代碼,在這個區(qū)域中的任何時候開始 GC 都是安全的辫樱。安全區(qū)域可以看作是安全點的拓展峭拘。

當(dāng)代碼執(zhí)行到安全區(qū)域時,首先標(biāo)識自己已經(jīng)進入了安全區(qū)域狮暑,那樣如果在這段時間里JVM發(fā)起GC鸡挠,就不用管標(biāo)示自己在安全區(qū)域的那些線程了,在線程離開安全區(qū)域時心例,會檢查系統(tǒng)是否正在執(zhí)行GC宵凌,如果是,就等到GC完成后再離開安全區(qū)域止后。


如何回收瞎惫?

主要的垃圾收集算法有下面三種:

標(biāo)記 - 清除算法

如同它的名字一樣,它只有兩個步驟译株,先是按照可達(dá)性算法標(biāo)記哪些內(nèi)存需要回收瓜喇,然后再一次性清理掉這些內(nèi)存。缺點也很明顯:一是效率問題歉糜,標(biāo)記和清除這兩個過程的效率都比較低乘寒;二是這種算法會產(chǎn)生很多內(nèi)存碎片。

復(fù)制算法

這種算法的核心是“復(fù)制”匪补,先把內(nèi)存分成大小相等的兩部分伞辛,每次都只使用其中的一部分烂翰,當(dāng)使用的那一部分需要內(nèi)存回收的時候,就將還存活著的對象復(fù)制到另一半上蚤氏,然后再將先前的那一半一次清理掉甘耿。這種方法簡單高效,解決了內(nèi)存碎片的問題竿滨,但缺點是每次都只使用總內(nèi)存的一半佳恬,浪費比較大。

標(biāo)記 - 復(fù)制算法示意圖

然而現(xiàn)在流行的商業(yè)虛擬機都是采用這種算法來回收新生代于游,不過不是將內(nèi)存空間1:1等分毁葱,而是將內(nèi)存分成一塊大的 Eden 分區(qū)和兩塊小的 Survivor 分區(qū),大小比例為8:1贰剥,每次使用 Eden 和其中的一塊 Survivor 分區(qū)倾剿。當(dāng)回收的時候,將 Eden 和 Survivor 中還存活的對象一次性復(fù)制到剩下的 Eden 分區(qū)上鸠澈。
類似下圖所示:
主流虛擬機采用的復(fù)制算法示意圖

為何可以這樣做呢柱告?IBM公司專門做過研究,新生代中的對象 98% 都是“朝生暮死的”笑陈,也就是說只有很少一部分的對象會在一次 GC 中存活下來际度,所以只占 10% 的 Survivor分區(qū)也是基本夠用的。不過涵妥,98% 的對象可回收只是一般情況下乖菱,無法保證每次存活的對象都只有不到 10%,那么就需要分配擔(dān)保了蓬网,意思就是當(dāng) Survivor 分區(qū)內(nèi)沒有足夠的空間存放上一次從新生代存活的對象的時候窒所,多出的這部分對象會直接進入老年代區(qū)域

標(biāo)記 - 整理算法

由于復(fù)制算法需要花很多時間進行存活對象的復(fù)制帆锋,效率比較低吵取,而且還需要分配擔(dān)保。而對于老年代區(qū)域的對象锯厢,沒有額外的區(qū)域進行分配擔(dān)保皮官,就無法使用復(fù)制算法,于是“標(biāo)記 - 整理”算法應(yīng)運而生实辑。

該算法的標(biāo)記過程與“標(biāo)記 - 清除”算法一樣捺氢,但后續(xù)步驟不是對可回收對象進行回收,而是讓存活的對象都向同一端移動剪撬,然后清理掉端邊界以外的內(nèi)存摄乒,如圖所示:
標(biāo)記 - 整理算法示意圖

分代收集算法

主流的商業(yè)虛擬機都采用“分代收集算法”,就是將內(nèi)存區(qū)域根據(jù)對象存活周期將內(nèi)存分成幾塊。具體是分成新生代和老年代馍佑,再根據(jù)不同的年代使用適合它們的垃圾收集算法斋否。

例如新生代中,每次垃圾收集會有大量的對象死去挤茄,存活的較少如叼,故可以采用復(fù)制算法。
老年代中穷劈,對象存活率高、沒有額外的空間進行分配擔(dān)保踊沸,故可以使用“標(biāo)記 - 清除”或“標(biāo)記 - 整理”算法歇终。


總結(jié)

本篇文章介紹了垃圾收集的三個核心部分:垃圾收集的對象的判定垃圾收集的時間垃圾收集的方法逼龟。下一節(jié)將介紹 HotSpot 虛擬機中幾種具體的垃圾收集器是如何實現(xiàn)的评凝。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市腺律,隨后出現(xiàn)的幾起案子奕短,更是在濱河造成了極大的恐慌,老刑警劉巖匀钧,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翎碑,死亡現(xiàn)場離奇詭異,居然都是意外死亡之斯,警方通過查閱死者的電腦和手機日杈,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來佑刷,“玉大人莉擒,你說我怎么就攤上這事√毙酰” “怎么了涨冀?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長麦萤。 經(jīng)常有香客問我鹿鳖,道長,這世上最難降的妖魔是什么频鉴? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任栓辜,我火速辦了婚禮,結(jié)果婚禮上垛孔,老公的妹妹穿的比我還像新娘藕甩。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布狭莱。 她就那樣靜靜地躺著僵娃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腋妙。 梳的紋絲不亂的頭發(fā)上默怨,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音骤素,去河邊找鬼匙睹。 笑死,一個胖子當(dāng)著我的面吹牛济竹,可吹牛的內(nèi)容都是我干的痕檬。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼送浊,長吁一口氣:“原來是場噩夢啊……” “哼梦谜!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起袭景,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤唁桩,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后耸棒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體荒澡,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年榆纽,在試婚紗的時候發(fā)現(xiàn)自己被綠了仰猖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡奈籽,死狀恐怖饥侵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情衣屏,我是刑警寧澤躏升,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站狼忱,受9級特大地震影響膨疏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜钻弄,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一佃却、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧窘俺,春花似錦饲帅、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽育八。三九已至,卻和暖如春赦邻,著一層夾襖步出監(jiān)牢的瞬間髓棋,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工惶洲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留按声,地道東北人。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓湃鹊,卻偏偏與公主長得像儒喊,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子币呵,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容