引言
C和C++ 程序員能夠直接操作內(nèi)存,憑自己的需要來決定何時去申請多大內(nèi)存,何時去釋放這塊內(nèi)存。他們甚至可以使用指針,來確定的去操作某塊內(nèi)存地址。作為后來者的JAVA遠(yuǎn)遠(yuǎn)不如他們強(qiáng)大,我認(rèn)識的JAVA程序員可能有半數(shù)都從未去關(guān)注過內(nèi)存。在使用JAVA進(jìn)行業(yè)務(wù)開發(fā)時,我們不需要去關(guān)注對象到底存儲在內(nèi)存的哪個位置,也不需要關(guān)注這個對象到底占用了多大的內(nèi)存空間,更不需要特意的為某個對象去釋放內(nèi)存空間。需要我們關(guān)注的,只有如何完成產(chǎn)品需求,在規(guī)定時間內(nèi)交付合格的產(chǎn)品給客戶。如果你對編程的看法跟上面所述的類似,或者你很認(rèn)同“完成比完美更重要”這條工程師信條,那么這篇文章就不太適合你來閱讀了。
內(nèi)存控制給編程界造成了一個具有魔性的圈,圈內(nèi)的人想出去,圈外的人想進(jìn)來。就像C程序員早就受夠了內(nèi)存的申請與釋放的折磨,受夠了各種內(nèi)存錯誤。而很多像你一樣的JAVA程序員,立志要寫出高性能應(yīng)用的人,一直在用心地思索怎么才能寫出性能更好的多線程應(yīng)用,當(dāng)這個追求達(dá)到一定層次的時候,你甚至已經(jīng)開始在乎JAVA垃圾回收所消耗的時間了。由于JAVA自身的限制,我們沒辦法進(jìn)入到內(nèi)存控制的圈內(nèi),但我們可以通過設(shè)置JVM參數(shù)等方式,選擇適當(dāng)?shù)睦占骰蛟O(shè)置垃圾回收線程對JAVA的垃圾回收機(jī)制進(jìn)行優(yōu)化。
概述
本文的重點(diǎn)是介紹JVM虛擬機(jī)下常用的垃圾回收算法的理論知識,并不介紹具體算法實(shí)現(xiàn)代碼。在閱讀本文之前,希望讀者已經(jīng)掌握了JVM的內(nèi)存劃分設(shè)計、對象引用的可達(dá)性分析算法與JAVA的四種對象引用級別(Strong Reference/Soft Reference/Weak Reference/Phantom Reference)等相關(guān)知識。在本文之后,我還會再去整理一些關(guān)于垃圾收集器等方面的知識。文中所涉及的代碼或理論都已在JDK8中進(jìn)行驗(yàn)證。
垃圾回收算法
標(biāo)記-清除算法(Mark-Sweep)
首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對象。
標(biāo)記-清除算法的工作過程如圖1所示。

圖1 標(biāo)記-清除算法示意圖
- 優(yōu)點(diǎn): 算法簡單清晰,其它垃圾收集算法都是據(jù)此算法的思想并對其不足改進(jìn)而得到的。
- 缺點(diǎn): 1.效率低,標(biāo)記和清除需要遍歷內(nèi)存,效率極低。2.回收內(nèi)存之后產(chǎn)生大量的內(nèi)存碎片,當(dāng)內(nèi)存碎片過多時,應(yīng)用需要給大對象分配內(nèi)存時無法分配連續(xù)內(nèi)存。
復(fù)制算法(Copying)
將內(nèi)存按容量劃分為完全等大小的兩塊,每次只使用其中一塊內(nèi)存。當(dāng)這一塊內(nèi)存用完后,將還活著的對象全部復(fù)制到另一塊上面,然后把這一塊上已使用的內(nèi)存空間全部清理掉。其示意圖如圖二所示。

圖2 復(fù)制算法示意圖
- 優(yōu)點(diǎn): 實(shí)現(xiàn)簡單,運(yùn)行高效。每次都是對整個半?yún)^(qū)進(jìn)行內(nèi)存回收,內(nèi)存分配時無需考慮內(nèi)存碎片等復(fù)雜情況。
- 缺點(diǎn): 浪費(fèi)內(nèi)存空間。在最壞的情況下,這種垃圾回收算法可能浪費(fèi)了一半的內(nèi)存空間。
在JAVA應(yīng)用中,90%之上的對象的生存時間都極短,所以JVM把內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次只使用Eden和其中一塊Survior空間。當(dāng)需要進(jìn)行垃圾回收時,將Eden和Survivor中還處于可達(dá)狀態(tài)的對象一次性的復(fù)制到另一塊Survivor空間中,最后清空Eden和剛剛使用的Survivor空間。當(dāng)Survivor空間不夠用時,還會將長時間存活的對象轉(zhuǎn)存的老年代中。在HotSpot虛擬機(jī)中,默認(rèn)的Eden和Survivor空間的比例是8:1,這樣可以做到只浪費(fèi)10%的內(nèi)存。
標(biāo)記-整理算法(Mark-Compact)
標(biāo)記-整理算法的標(biāo)記過程與標(biāo)記-清除算法的標(biāo)記過程類似,但在標(biāo)記完成后并不是直接對可回收的對象進(jìn)行清理,而是讓所有正在存活的對象都向前端移動,然后直接清理掉邊界以外的內(nèi)存。其示意圖見圖3.

圖3 標(biāo)記-整理算法示意圖
- 優(yōu)點(diǎn): 節(jié)省內(nèi)存空間,提升內(nèi)存運(yùn)用率,且不會產(chǎn)生內(nèi)存碎片。
- 缺點(diǎn): 性能低。
分代收集算法(Generational Collection)
根據(jù)對象的存活時間把內(nèi)存分為新生代和老年代,根據(jù)個代對象的存活特點(diǎn),每個代采用不同的垃圾回收算法。
分代收集就是根據(jù)不同代的特性,使用最合適的垃圾回收算法進(jìn)行垃圾回收。如新生代中,每次垃圾收集都會有大量的對象死去,只有極小部分對象存活,所以更適合復(fù)制算法。老年代中對象存活率高,且沒有額外的內(nèi)存空間為它進(jìn)行分配擔(dān)保,所以更適合用標(biāo)記-清除或標(biāo)記-整理算法。
HotSpot算法實(shí)現(xiàn)
枚舉根節(jié)點(diǎn)(GC Roots)
在垃圾回收時,我們要想辦法找出哪些對象是存活的,一般會選取一些被稱為GC Root的對象,從這些對象開始枚舉。在進(jìn)行GC Root枚舉時要求所有對象停下來,也就是JVM所稱的“Stop the world”。所有的算法實(shí)現(xiàn)都會將虛擬機(jī)停下來的,否則分析結(jié)果的準(zhǔn)確性將無法保證。
由于HotSpot采用準(zhǔn)確式GC,該技術(shù)主要功能就是讓虛擬機(jī)可以準(zhǔn)確的知道內(nèi)存中某個位置的數(shù)據(jù)類型是什么,比如某個內(nèi)存位置到底是一個整型的變量,還是對某個對象的 reference。這樣在進(jìn)行 GC Roots 枚舉時,只需要枚舉 reference 類型的即可。在能夠準(zhǔn)確地確定 Java 堆和方法區(qū)等 reference 準(zhǔn)確位置之后,HotSpot 就能極大地縮短 GC Roots 枚舉時間,所以當(dāng)執(zhí)行系統(tǒng)停頓下來之后,虛擬機(jī)不需要遍歷所有的根節(jié)點(diǎn)和上下文去確定GC Roots,而是存在著一個OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個目的。
在類加載完成的時候,虛擬機(jī)就會把什么類的什么偏移上是什么類型的數(shù)據(jù)計算出來。在JIT編譯的時候也會在特定位置記下在寄存器和棧中哪些位置是引用,GC在掃描時就可直接得到信息。
安全點(diǎn)(Safepoint)
Safepoint:會導(dǎo)致 OopMap 內(nèi)容變化的指令非常多,如果為每一條指令都生成對應(yīng)的 OopMap,那么將需要大量的額外空間,這樣對導(dǎo)致 GC 成本很高,所以 HotSpot 只在 “特定位置” 記錄這些信息,這些位置被稱為 安全點(diǎn)(Safepoint)。并非程序在任意時刻都可以停頓下來進(jìn)行 GC,而只有程序到達(dá) 安全點(diǎn)(Safepoint) 以后才可以停頓下來進(jìn)行 GC;所以安全點(diǎn)既不能太少,以至于 GC 過程等待程序到達(dá)安全點(diǎn)的時間過長,也不能太多,以至于 GC 過程帶來的成本過高。
由于在 GC 過程中必須保證程序已執(zhí)行,那么也就是說 必須等待所有線程都到達(dá)安全點(diǎn)上方可進(jìn)行 GC。一般來說有兩種解決方案可以選擇:
搶先式中斷:不需要線程的執(zhí)行代碼去主動配合,當(dāng)發(fā)生 GC 時,先強(qiáng)制中斷所有線程,然后如果發(fā)現(xiàn)某些線程未處于安全點(diǎn),那么將其喚醒,直至其到達(dá)安全點(diǎn)再次將其中斷;這樣一直等待所有線程都在安全點(diǎn)后開始 GC?,F(xiàn)在幾乎沒有虛擬機(jī)使用這種方式。
主動式中斷:不強(qiáng)制中斷線程,只是簡單地設(shè)置一個中斷標(biāo)記,各個線程在執(zhí)行時輪詢這個標(biāo)記,一旦發(fā)現(xiàn)標(biāo)記被改變(出現(xiàn)中斷標(biāo)記)時,那么將運(yùn)行到安全點(diǎn)后自己中斷掛起;目前所有商用虛擬機(jī)全部采用主動式中斷。
安全區(qū)(Safe Region)
安全點(diǎn)機(jī)制僅僅是保證了程序執(zhí)行時不需要太長時間就可以進(jìn)入一個安全點(diǎn)進(jìn)行 GC 動作,但是當(dāng)特殊情況時,比如線程休眠、線程阻塞等狀態(tài)的情況下,顯然 JVM 不可能一直等待被阻塞或休眠的線程正常喚醒執(zhí)行,此時就引入了安全區(qū)的概念。
安全區(qū)(Safe Region):安全區(qū)域是指在一段代碼區(qū)域內(nèi),對象引用關(guān)系等不會發(fā)生變化,在此區(qū)域內(nèi)任意位置開始 GC 都是安全的。線程運(yùn)行到Safe Region中的代碼時,首先標(biāo)記自己進(jìn)入了安全區(qū),然后在這段區(qū)域內(nèi),如果線程發(fā)生了阻塞、休眠等操作,JVM 發(fā)起 GC 時將忽略這些處于安全區(qū)的線程。當(dāng)線程再次被喚醒時,首先他會檢查是否完成了 GC Roots枚舉(或這個GC過程),然后選擇是否繼續(xù)執(zhí)行,否則將繼續(xù)等待 GC 的完成。
引用
本文是對class文件的學(xué)習(xí)筆記,筆記的內(nèi)容并非是原創(chuàng),而是大量參考其它資料。在寫作本文的過程中引用了以下資料,為為在此深深謝過以下資料的作者。
- 《The Java? Virtual Machine Specification · Java SE 8 Edition》
- 《深入理解Java虛擬機(jī):JVM高級特性與最佳實(shí)踐/周志明著.——2版.——北京:機(jī)械工業(yè)出版社,2013.6》
- 《Java Platform, Standard Edition HotSpot Virtual Machine Garbage Collection Tuning Guide》
非原創(chuàng)聲明
>本文并非我的原創(chuàng)文章,而是我學(xué)習(xí)jvm時的筆記。文中的材料與數(shù)據(jù)大部分來自于其它資料,詳細(xì)請查看本文的引用章節(jié)。
關(guān)于
本項目和文檔中所用的內(nèi)容僅供學(xué)習(xí)和研究之用,轉(zhuǎn)載或引用時請指明出處。如果你對文檔有疑問或問題,請在項目中給我留言或發(fā)email到 weiwei02@vip.qq.com
我的github: https://github.com/weiwei02/
我相信技術(shù)能夠改變世界。