深入理解JVM讀書筆記
從半個多世紀(jì)前的Lisp語言開始,垃圾回收機制正式登上歷史舞臺薄坏。但是直到今天新啼,仍然沒有一個完美的垃圾回收方案伤靠。從Java誕生到現(xiàn)如今的Java 8, Java的垃圾回收管理機制也一直在嘗試優(yōu)化戴卜。雖然到今天位置垃圾回收機制并不完美逾条,但是它仍舊是目前人類頂尖智慧的結(jié)晶,其中的一些設(shè)計不得不讓人佩服投剥。
說起到垃圾回收機制师脂,我們不得不思考三個問題:
- 哪些內(nèi)存需要回收?
- 如何進行回收?
- 什么時間進行回收吃警?
一. 哪些內(nèi)存需要回收糕篇?
1. 引用計數(shù)算法
很多教科書中把引用計數(shù)算法作為判斷對象是否存活的算法:“給每個對象添加一個計數(shù)器,當(dāng)使用該引用引用時計數(shù)器值加1酌心,當(dāng)引用失效時計數(shù)器值減1娩缰;當(dāng)某個對象的引用計數(shù)器值為0時,表示該對象可以被回收谒府。
客觀的來說引用技術(shù)算法實現(xiàn)簡單,效率也不低浮毯;但是它卻有一個致命的問題:很難解決對象循環(huán)引用的問題完疫。
在JVM中,循環(huán)引用并不會影響對象的回收债蓝。所以JVM中并不是通過引用技術(shù)算法來判斷對象是否能被回收壳鹤。
//TODO: Code
2. 可達性分析算法
在主流商用語言(Java,C#等)中饰迹,一般是通過可達性分析來判斷對象是否可被回收芳誓。從 “GC Root” 對象作為起點開始搜索,搜索所走過的路徑成為引用鏈啊鸭。如果一個對象從GC Root開始無引用鏈可達锹淌,則表示這個對象是不被使用的,是可被回收的赠制。
那GC Root到底是些什么樣的對象呢赂摆? 在Java中可以做GC Root的對象包括 本文不會對JVM內(nèi)存模型進行解析,如果大家有疑問可以自行參考相關(guān)文章:
- 虛擬機棧中引用的對象
- 方法區(qū)中類靜態(tài)屬性引用的對象
- 方法區(qū)中常亮引用的對象
- 本地方法棧中JNI引用的對象
3. finalize() 方法
如果一個對象通過可達性分析算法判定為不可達對象钟些,那么等待它的就只是被殺死的命運嗎烟号?然而并不是,咸魚都有翻身的機會政恍,對象也該有它的機會汪拥!
一個對象如果被判定為“不可達”狀態(tài),系統(tǒng)會對它進行篩選篙耗,判斷對象是否需要執(zhí)行finalize() 方法迫筑。 如果對象沒有覆蓋finalize()方法,或者finalize()方法已經(jīng)被系統(tǒng)調(diào)用過一次 [finalize()方法只會被調(diào)用一次]鹤树,拿它就會被判定為不需要執(zhí)行finalize(),也就失去了最后翻身的機會铣焊。
相反如果對象被判定為有必要執(zhí)行finalize()方法,那么這個對象會被放到一個F-Queue的隊列中罕伯。稍后系統(tǒng)會建立一個優(yōu)先級非常低的線程去調(diào)用對象的finalize()方法曲伊。如果在finalize()方法中,對象重新連接上引用鏈,那么在第二次標(biāo)記時它會被移除“即將被回收”的集合坟募,否則它將被回收岛蚤。
//TODO: code
二. 如何進行回收?
1. 垃圾收集算法
垃圾回收算法有很多種懈糯,每一種算法的細(xì)節(jié)又非常復(fù)雜涤妒。在此我們盡量避免介紹一些算法細(xì)節(jié),只是介紹下主流垃圾回收算法的思想赚哗。
1.1 標(biāo)記-清除算法
法如其名她紫,首先將所有需要回收的對象做標(biāo)記【標(biāo)記過程即是上文提到的標(biāo)記】,標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記對象屿储。
標(biāo)記清除算法是最基礎(chǔ)的收集算法贿讹。因為后續(xù)的收集算法都是基于標(biāo)記清除算法,并嘗試解決它的兩個不足:
- 效率問題够掠,標(biāo)記和清除效率都不高
- 控件問題民褂,清除后會產(chǎn)生很多不連續(xù)的內(nèi)存碎片
//TODO: Image
1.2 復(fù)制算法
Copy算法很大程度上是為了解決效率問題而誕生的。它將可用內(nèi)存平分為兩部分疯潭。每次只使用其中的一部分赊堪,當(dāng)這部分內(nèi)存將要用完時觸發(fā)回收,之后將存活的對象復(fù)制到另一部分內(nèi)存上并清理掉之前的內(nèi)存竖哩。
它的優(yōu)勢:每次都是對一半的區(qū)域進行回收哭廉;不需要考慮內(nèi)存碎片問題,內(nèi)存分配簡單高效相叁。缺點也很明顯:可用內(nèi)存空間只能是全部內(nèi)存的一半群叶。
復(fù)制算法在實際應(yīng)用中使用的比較多,它常被一些商業(yè)虛擬機用來做對新生代的回收钝荡。但是實際上內(nèi)存分塊并不是按照1:1來分配的街立,因為IBM研究表明98%新生代中的對象都是可被回收的,也就是2%的對象是需要被拷貝的埠通。在復(fù)制算法的升級版本中內(nèi)存被分為一個大的Eden區(qū)和兩個小的Survivor區(qū)赎离,每次使用Eden和一個Survivor區(qū)。當(dāng)GC時端辱,講Eden和Survivor中存活的對象Copy到另一個Survivor區(qū)中梁剔。在HotPot虛擬機中Eden和Survivor的默認(rèn)比例是8:1,也就是新生代的可用內(nèi)存為整個新生代內(nèi)存總量的90%舞蔽。一個Survivor區(qū)可用內(nèi)存只有10%的總?cè)萘咳俨。窍到y(tǒng)并不能保證每次回收完之后剩余對象大小一定在10%以內(nèi)。在此種情況下Survivor區(qū)控件不足時渗柿,系統(tǒng)需要依賴其他內(nèi)存(老年代)進行分配擔(dān)保(Handle Promotion)个盆。
//TODO: Image
1.3 標(biāo)記整理算法
復(fù)制算法在新生代回收中非常給力脖岛,但是在老年代中對象存活率較高,對象的多次拷貝效率會降低颊亮,并且因為對象存活率比較高柴梆,無法像新生代一樣設(shè)置8:1的比例一般只能按照1:1來設(shè)置,導(dǎo)致老年代內(nèi)存的使用率只能是50%左右终惑。所以在老年代中一般不會直接使用復(fù)制算法绍在。
標(biāo)記清理算法正是專門針對老年代的特點而產(chǎn)生的。它跟標(biāo)記-清除算法類似雹有。只是標(biāo)記完成后不是直接回收清除偿渡,而是讓所有存活的對象都像一端移動,然后直接清理其他所有空間霸奕。
1.4 分代收集算法
當(dāng)前普遍使用的分代收集算法并不能算是一個獨立的算法卸察。它更像是一種機制。它只是根據(jù)對象存活周期的不同將內(nèi)存分為新生代和老年代铅祸。而新生代和老年代具體的內(nèi)存回收算法則是前面介紹的算法來實現(xiàn)。
2. 垃圾收集器
垃圾收集器是JVM中對垃圾回收算法的具體實現(xiàn)合武。不同廠商临梗,不同的虛擬機版本所提供的垃圾收集器都可能有很大的不同,我們只對其中比較有代表性的幾個垃圾收集器做介紹稼跳。
2.1 Serial盟庞、Serial Old
Serial收集器是最基本,歷史最悠久的收集器汤善,一般用于分代收集中的新生代收集什猖。見名知意,它是一個單線程的垃圾收集器红淡。并且Serial還有一層含義是不狮,它在進行垃圾回收時必須停止其他所有工作線程(Stop The World),然后使用Copy算法進行垃圾回收在旱,最后恢復(fù)其他工作線程摇零。
Serial Old是Serial的老年代版本桶蝎。它跟Serial機制差不多,同樣是一個單線程收集器登渣,不過它是通過標(biāo)記-整理算法來實現(xiàn)的。
2.2 ParNew胜茧、Parallel Scavenge粘优、Parallel Old
Parallel系列收集器都是通過多條線程進行垃圾回收的。
ParNew與Serial相比除了使用多線程外敬飒,并沒有太多的創(chuàng)新之處(Copy算法)。它是很多Server模式虛擬機的新生代收集器无拗,因為它能很好的與老年代的CMS收集器配合工作。
Parallel Scavenge收集器跟ParNew很像英染,使用Copy算法多線程回收。它的特點是它是一個吞吐量優(yōu)先收集器四康,吞吐量=運行用戶代碼時間/(運行用戶代碼時間+GC時間)搪搏。Parallel Scavenge提供的參數(shù)可以控制Eden和Survivor的空間大小和比例疯溺,進而控制每次GC所用時間和系統(tǒng)總吞吐量。
Parallel Old收集器是Parallel Scavenge的老年代版本哎垦。它使用標(biāo)記整理算法多線程回收囱嫩。它一般用作吞吐量優(yōu)先收集器的老年代版本,跟Parallel Scavenge搭配自稱吞吐量優(yōu)先組合漏设。
2.3 CMS
CMS(Concurrent Mark Sweep)收集器是一種為了盡量縮短停頓時間的收集器墨闲。為了追求更好的用戶體驗,對服務(wù)響應(yīng)速度的要求也會相應(yīng)的提高郑口。CMS也就應(yīng)運而生鸳碧。 CMS采用MS(Mark-Sweep)標(biāo)記清除算法進行垃圾回收。CMS回收過程分為四個階段:
- 初始標(biāo)記
- 并發(fā)標(biāo)記
- 重新標(biāo)記
- 并發(fā)清除
其中初始標(biāo)記和重新標(biāo)記仍然需要Stop The World犬性。初始標(biāo)記僅僅是標(biāo)記GC Root直接關(guān)聯(lián)到的對象船响,速度很快迅办。并發(fā)標(biāo)記就是做GC Root引用鏈的搜索跑筝,重新標(biāo)記階段是為了糾正并發(fā)階段期間可能由于代碼執(zhí)行導(dǎo)致的標(biāo)記變化南捂。耗時最長的并發(fā)標(biāo)記和并發(fā)清除階段都可以與用戶工作線程并發(fā)執(zhí)行,所以總體來看CMS基本上是與用戶線程并發(fā)執(zhí)行的缸兔。
當(dāng)然CMS并不是一個完美的垃圾收集器日裙。它的并發(fā)會讓它對CPU資源敏感,CPU數(shù)較少時GC會占用比較多的CPU資源惰蜜。另一方面由于是基于標(biāo)記清除算法昂拂,它只能通過Full GC(收集整個堆,不管新生代老年代)對內(nèi)存空間碎片進行處理抛猖。
2.4 G1
G1(Garbage-First) 收集器是當(dāng)今收集器技術(shù)發(fā)展的最前沿成果之一格侯。但是因為是從JDK7u4版本開始移除的Experimental標(biāo)記鼻听,目前在實際生產(chǎn)環(huán)境中使用并不多。
其他的收集器收集的范圍都是整個新生代或者老年代联四。而G1將內(nèi)存區(qū)進一步劃分為若干個大小相等的獨立區(qū)域(Region)撑碴。而新生代和老年代不再是物理隔離的,而分別是一部分不需要連續(xù)性的Region的集合朝墩。G1可以跟蹤并維護每個Region的回收價值(回收所得空間大小與回收所費時間的經(jīng)驗值)醉拓,每次回收優(yōu)先回收價值最大的區(qū)域,也也就是Garbage-First的由來收苏。
G1收集器收集過程也是分為4個階段亿卤,前三個階段與CMS相同,最后階段由并發(fā)清除變?yōu)楹Y選回收鹿霸。所謂篩選回收就是計算Region的回收價值并排序排吴,選擇價值最高的Region進行回收。
它與CMS等其他收集器相比有以下的一些優(yōu)勢:
- 并行與并發(fā):充分利用多核或多CPU技術(shù)來縮短Stop The World 的時間懦鼠。
- 分代收集: G1可以獨立管理新老代钻哩,也可以與某些其他收集器配合使用。
- 空間整合: 與CMS的標(biāo)記-清理算法不同肛冶,整體來看G1是基于標(biāo)記-整理算法的收集器街氢,從局部來看Region間是基于Copy算法實現(xiàn)的。無論如何都是不會產(chǎn)生內(nèi)存碎片的淑趾。
- 可預(yù)測的停頓:G1和CMS都致力于降低停頓時間。G1顯然技高一籌忧陪。G1允許使用者明確指定M時間段內(nèi)扣泊,消耗在GC的時間不得超過N。
三. 垃圾回收的時機
1. 安全點
在GC開始時嘶摊,系統(tǒng)會停頓下來做枚舉根節(jié)點的操作延蟹。虛擬機一般會維護一個數(shù)據(jù)結(jié)構(gòu),來保存當(dāng)前執(zhí)行上下文和全局的引用位置叶堆。在HotSpot虛擬機中是通過OopMap這個數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)的阱飘。在OopMap的幫助下,HotPots可以快速精準(zhǔn)的完成GC Root枚舉虱颗。
但是問題來了:有很多指令都可能導(dǎo)致OopMap的內(nèi)容變化沥匈,如果為每一條指令都生成對應(yīng)的OopMap會占用大量的額外空間成本。事實上HotPots也不會為每條指令生成OopMap忘渔,只是在“某些特定位置”生成了這些信息高帖。這些位置就是所謂的安全點,也就是系統(tǒng)并不會隨時隨地在任何地方都能停頓下來做GC操作畦粮,只有在安全點才能散址。
安全點太少會導(dǎo)致GC等待時間過長乖阵,安全點過多會增加系統(tǒng)運行成本预麸。那如何選擇安全點呢? 安全點的選擇一般以“是否有讓程序長時間運行的特征”為標(biāo)準(zhǔn)吏祸。例如在方法調(diào)用,循環(huán)跳轉(zhuǎn)犁罩,異常跳轉(zhuǎn)等地方才會有安全點。
為了保證GC發(fā)生時所有線程都能在安全點床估,系統(tǒng)提供搶先式中斷和主動式中斷來實現(xiàn)含滴。搶先式中斷就是所有線程先中斷丐巫,然后讓不在安全點的線程恢復(fù)并運行到安全點。主動式中斷就是系統(tǒng)不直接中斷線程而是只設(shè)置一個標(biāo)記位递胧,其他線程執(zhí)行時主動輪詢這個標(biāo)記位。
2. 安全區(qū)域
安全點解決了如何進入GC的問題缎脾,保證程序執(zhí)行不長時間都會觸發(fā)一次GC。但是如果程序沒有執(zhí)行呢遗菠,如果線程處于Sleep或Blockd狀態(tài)呢联喘?這種情況就需要安全區(qū)域來解決辙纬。 安全區(qū)域是指引用關(guān)系不會發(fā)生變化的一段代碼片段。在這個區(qū)域的任何地方進行GC都是安全的贺拣。線程進入安全區(qū)域后會先標(biāo)識自己進入了安全區(qū)域,系統(tǒng)GC時就會認(rèn)為此線程是回收安全狀態(tài)譬涡。當(dāng)線程要離開安全區(qū)域時,會檢查系統(tǒng)是否完成了根節(jié)點枚舉才決定是否能離開涡匀。