今天我們來(lái)談?wù)凧ava主流虛擬機(jī)-HotSpot的GC實(shí)現(xiàn)機(jī)制,本篇文章默認(rèn)使用HotSpot虛擬機(jī)進(jìn)行介紹锉走,如果沒(méi)有特殊說(shuō)明癌瘾,其都為HotSpot虛擬機(jī)中的特性。
? Java與C++之間有一堵由內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)所圍城的“高墻”盖腕,墻外面的人想進(jìn)去,墻里面的人卻想出來(lái)浓镜。
說(shuō)起垃圾收集溃列,大部分人都把這項(xiàng)技術(shù)當(dāng)做Java語(yǔ)言的伴生產(chǎn)物。事實(shí)上膛薛,GC的歷史比Java久遠(yuǎn)馒稍,1960年誕生與MIT的Lisp是第一門(mén)真正使用內(nèi)存動(dòng)態(tài)分配和垃圾收集技術(shù)的語(yǔ)言。關(guān)于Garbage Collection的歷史這里就不多說(shuō)了懦底,因?yàn)檫@是一篇技術(shù)博客而不是來(lái)將歷史的,如果對(duì)GC的發(fā)展歷史感興趣可以自行百度风范。
一、GC實(shí)現(xiàn)機(jī)制-我們?yōu)槭裁匆チ私釭C和內(nèi)存分配沪么?
? ? ? 說(shuō)道這個(gè)問(wèn)題硼婿,我有一個(gè)簡(jiǎn)單的回答:在真實(shí)工作中的項(xiàng)目中,時(shí)不時(shí)的會(huì)發(fā)生內(nèi)存溢出禽车、內(nèi)存泄露的問(wèn)題寇漫,這也是不可避免的Bug,這些潛在的Bug在某些時(shí)候會(huì)影響到項(xiàng)目的正常運(yùn)行殉摔,如果你的項(xiàng)目沒(méi)有合理的進(jìn)行業(yè)務(wù)內(nèi)存分配州胳,將會(huì)直接影響到的項(xiàng)目的并發(fā)處理,當(dāng)垃圾收集成為系統(tǒng)達(dá)到更高并發(fā)量的瓶頸時(shí)逸月,我們就需要對(duì)這些“自動(dòng)化”的技術(shù)實(shí)施必要的監(jiān)控和調(diào)節(jié)陋葡,而了解了GC實(shí)現(xiàn)機(jī)制則是我們一切監(jiān)控和調(diào)節(jié)的前提。
二彻采、GC實(shí)現(xiàn)機(jī)制-Java虛擬機(jī)將會(huì)在什么地方進(jìn)行垃圾回收腐缤?
? ? ?說(shuō)起垃圾回收的場(chǎng)所,了解過(guò)JVM(Java Virtual Machine Model)內(nèi)存模型的朋友應(yīng)該會(huì)很清楚肛响,堆是Java虛擬機(jī)進(jìn)行垃圾回收的主要場(chǎng)所岭粤,其次要場(chǎng)所是方法區(qū)。
三特笋、GC實(shí)現(xiàn)機(jī)制-Java虛擬機(jī)具體實(shí)現(xiàn)流程
我們都知道在Java虛擬機(jī)中進(jìn)行垃圾回收的場(chǎng)所有兩個(gè)剃浇,一個(gè)是堆,一個(gè)是方法區(qū)猎物。在堆中存儲(chǔ)了Java程序運(yùn)行時(shí)的所有對(duì)象信息虎囚,而垃圾回收其實(shí)就是對(duì)那些“死亡的”對(duì)象進(jìn)行其所侵占的內(nèi)存的釋放,讓后續(xù)對(duì)象再能分配到內(nèi)存蔫磨,從而完成程序運(yùn)行的需要淘讥。關(guān)于何種對(duì)象為死亡對(duì)象,在下一部分將做詳細(xì)介紹堤如。Java虛擬機(jī)將堆內(nèi)存進(jìn)行了“分塊處理”蒲列,從廣義上講,在堆中進(jìn)行垃圾回收分為新生代(Young Generation)和老生代(Old Generation)搀罢;從細(xì)微之處來(lái)看蝗岖,為了提高Java虛擬機(jī)進(jìn)行垃圾回收的效率,又將新生代分成了三個(gè)獨(dú)立的區(qū)域(這里的獨(dú)立區(qū)域只是一個(gè)相對(duì)的概念榔至,并不是說(shuō)分成三個(gè)區(qū)域以后就不再互相聯(lián)合工作了)抵赢,分別為:Eden區(qū)(Eden Region)、From Survivor區(qū)(Form Survivor Region)以及To Survivor(To Survivor Region),而Eden區(qū)分配的內(nèi)存較大铅鲤,其他兩個(gè)區(qū)較小划提,每次使用Eden和其中一塊Survivor。Java虛擬機(jī)在進(jìn)行垃圾回收時(shí)彩匕,將Eden和Survivor中還存活著的對(duì)象進(jìn)行一次性地復(fù)制到另一塊Survivor空間上腔剂,直到其兩個(gè)區(qū)域中對(duì)象被回收完成媒区,當(dāng)Survivor空間不夠用時(shí)驼仪,需要依賴(lài)其他老年代的內(nèi)存進(jìn)行分配擔(dān)保。當(dāng)另外一塊Survivor中沒(méi)有足夠的空間存放上一次新生代收集下來(lái)的存活對(duì)象時(shí)袜漩,這些對(duì)象將直接通過(guò)分配擔(dān)保機(jī)制進(jìn)入老生代绪爸,在老生代中不僅存放著這一種類(lèi)型的對(duì)象,還存放著大對(duì)象(需要很多連續(xù)的內(nèi)存的對(duì)象)宙攻,當(dāng)Java程序運(yùn)行時(shí)奠货,如果遇到大對(duì)象將會(huì)被直接存放到老生代中,長(zhǎng)期存活的對(duì)象也會(huì)直接進(jìn)入老年代座掘。如果老生代的空間也被占滿递惋,當(dāng)來(lái)自新生代的對(duì)象再次請(qǐng)求進(jìn)入老生代時(shí)就會(huì)報(bào)OutOfMemory異常。新生代中的垃圾回收頻率高溢陪,且回收的速度也較快萍虽。就GC回收機(jī)制而言,JVM內(nèi)存模型中的方法區(qū)更被人們傾向的稱(chēng)為永久代(Perm Generation)形真,保存在永久代中的對(duì)象一般不會(huì)被回收杉编。其永久代進(jìn)行垃圾回收的頻率就較低,速度也較慢咆霜。永久代的垃圾收集主要回收廢棄常量和無(wú)用類(lèi)邓馒。以String常量abc為例,當(dāng)我們聲明了此常量蛾坯,那么它就會(huì)被放到運(yùn)行時(shí)常量池中光酣,如果在常量池中沒(méi)有任何對(duì)象對(duì)abc進(jìn)行引用,那么abc這個(gè)常量就算是廢棄常量而被回收脉课;判斷一個(gè)類(lèi)是否“無(wú)用”挂疆,則需同時(shí)滿足三個(gè)條件:
? ? ? ? ? (1)、該類(lèi)所有的實(shí)例都已經(jīng)被回收下翎,也就是Java堆中不存在該類(lèi)的任何實(shí)例缤言;
? ? ? ? ? (2)、加載該類(lèi)的ClassLoader已經(jīng)被回收
? ? ? ? ? (3)视事、該類(lèi)對(duì)應(yīng)的java.lang.Class對(duì)象沒(méi)有在任何地方被引用胆萧,無(wú)法在任何地方通過(guò)反射訪問(wèn)該類(lèi)的方法。
虛擬機(jī)可以對(duì)滿足上述3個(gè)條件的無(wú)用類(lèi)進(jìn)行回收,這里說(shuō)的是可以回收而不是必然回收跌穗。
? ? 大多數(shù)情況下订晌,對(duì)象在新生代Eden區(qū)中分配,當(dāng)Eden區(qū)沒(méi)有足夠空間進(jìn)行分配時(shí)蚌吸,虛擬機(jī)將發(fā)起一次Minor GC锈拨;同理,當(dāng)老年代中沒(méi)有足夠的內(nèi)存空間來(lái)存放對(duì)象時(shí)羹唠,虛擬機(jī)會(huì)發(fā)起一次Major GC/Full GC奕枢。只要老年代的連續(xù)空間大于新生代對(duì)象總大小或者歷次晉升的平均大小就會(huì)進(jìn)行Minor GC,否則將進(jìn)行Full CG佩微。
? ? 虛擬機(jī)通過(guò)一個(gè)對(duì)象年齡計(jì)數(shù)器來(lái)判定哪些對(duì)象放在新生代缝彬,哪些對(duì)象應(yīng)該放在老生代。如果對(duì)象在Eden出生并經(jīng)過(guò)一次Minor GC后仍然存活哺眯,并且能被Survivor容納的話谷浅,將被移動(dòng)到Survivor空間中,并將該對(duì)象的年齡設(shè)為1奶卓。對(duì)象每在Survivor中熬過(guò)一次Minor GC一疯,年齡就增加1歲,當(dāng)他的年齡增加到最大值15時(shí)夺姑,就將會(huì)被晉升到老年代中墩邀。虛擬機(jī)并不是永遠(yuǎn)地要求對(duì)象的年齡必須達(dá)到MaxTenuringThreshold才能晉升到老年代,如果在Survivor空間中所有相同年齡的對(duì)象大小的總和大于Survivor空間的一半瑟幕,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代磕蒲,無(wú)需等到MaxTenuringThreshold中要求的年齡。
四只盹、GC實(shí)現(xiàn)機(jī)制-Java虛擬機(jī)如何實(shí)現(xiàn)垃圾回收機(jī)制
? ?(1)辣往、引用計(jì)數(shù)算法(Reference Counting)
? ? ? ? ? ? ?給對(duì)象添加一個(gè)引用計(jì)數(shù)器,每當(dāng)有一個(gè)地方引用它時(shí)殖卑,計(jì)數(shù)器值就加1站削;當(dāng)引用失效時(shí),計(jì)數(shù)器值就減1孵稽;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的许起,這就是引用計(jì)數(shù)算法的核心∑邢剩客觀來(lái)講园细,引用計(jì)數(shù)算法實(shí)現(xiàn)簡(jiǎn)單,判定效率也很高接校,在大部分情況下都是一個(gè)不錯(cuò)的算法猛频。但是Java虛擬機(jī)并沒(méi)有采用這個(gè)算法來(lái)判斷何種對(duì)象為死亡對(duì)象,因?yàn)樗茈y解決對(duì)象之間相互循環(huán)引用的問(wèn)題。
public class ReferenceCountingGC{
?????????public Object object = null;
????????? private static final int OenM = 1024 * 1024;
????????? private byte[] bigSize = new byte[2 * OneM];
? public static void testCG(){
? ? ???????? ReferenceCountingGC objA = new ReferenceCountingGC();
? ? ? ????????ReferenceCountingGC objB = new ReferenceCountingGC();
? ? ? objA.object = null;
? ? ? objB.object = null;
? ? System.gc();
????????}
}
? ? 在上述代碼段中鹿寻,objA與objB互相循環(huán)引用睦柴,沒(méi)有結(jié)束循環(huán)的判斷條件,運(yùn)行結(jié)果顯示Full GC毡熏,就說(shuō)明當(dāng)Java虛擬機(jī)并不是使用引用計(jì)數(shù)算法來(lái)判斷對(duì)象是否存活的坦敌。
? ? (2)、可達(dá)性分析算法(Reachability Analysis)
? ? ? ? ? ? 這是Java虛擬機(jī)采用的判定對(duì)象是否存活的算法痢法。通過(guò)一系列的稱(chēng)為“GC Roots"的對(duì)象作為起始點(diǎn)狱窘,從這些結(jié)點(diǎn)開(kāi)始向下搜索,搜索所走過(guò)的路徑稱(chēng)為飲用鏈(Reference Chain)疯暑,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí)训柴,則證明此對(duì)象是不可用的哑舒「菊可作為GC Roots的對(duì)象包括:虛擬機(jī)棧中引用的對(duì)象、方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象洗鸵、方法區(qū)中常量引用的對(duì)象越锈。本地方法棧JNI引用的對(duì)象。
在上圖可以看到GC Roots左邊的對(duì)象都有引用鏈相關(guān)聯(lián)膘滨,所以他們不是死亡對(duì)象甘凭,而在GCRoots右邊有幾個(gè)零散的對(duì)象沒(méi)有引用鏈相關(guān)聯(lián),所以他們就會(huì)別Java虛擬機(jī)判定為死亡對(duì)象而被回收火邓。
五丹弱、GC實(shí)現(xiàn)機(jī)制-何為死亡對(duì)象?
? ? ? Java虛擬機(jī)在進(jìn)行死亡對(duì)象判定時(shí)铲咨,會(huì)經(jīng)歷兩個(gè)過(guò)程躲胳。如果對(duì)象在進(jìn)行可達(dá)性分析后沒(méi)有與GC Roots相關(guān)聯(lián)的引用鏈,則該對(duì)象會(huì)被JVM進(jìn)行第一次標(biāo)記并且進(jìn)行一次篩選纤勒,篩選的條件是此對(duì)象是否有必要執(zhí)行finalize()方法坯苹,如果當(dāng)前對(duì)象沒(méi)有覆蓋該方法,或者finalize方法已經(jīng)被JVM調(diào)用過(guò)都會(huì)被虛擬機(jī)判定為“沒(méi)有必要執(zhí)行”摇天。如果該對(duì)象被判定為沒(méi)有必要執(zhí)行粹湃,那么該對(duì)象將會(huì)被放置在一個(gè)叫做F-Queue的隊(duì)列當(dāng)中,并在稍后由一個(gè)虛擬機(jī)自動(dòng)建立的泉坐、低優(yōu)先級(jí)的Finalizer線程去執(zhí)行它为鳄,在執(zhí)行過(guò)程中JVM可能不會(huì)等待該線程執(zhí)行完畢,因?yàn)槿绻粋€(gè)對(duì)象在finalize方法中執(zhí)行緩慢腕让,或者發(fā)生死循環(huán)孤钦,將很有可能導(dǎo)致F-Queue隊(duì)列中其他對(duì)象永久處于等待狀態(tài),甚至導(dǎo)致整個(gè)內(nèi)存回收系統(tǒng)崩潰。如果在finalize方法中該對(duì)象重新與引用鏈上的任何一個(gè)對(duì)象建立了關(guān)聯(lián)司训,即該對(duì)象連上了任何一個(gè)對(duì)象的引用鏈构捡,例如this關(guān)鍵字,那么該對(duì)象就會(huì)逃脫垃圾回收系統(tǒng)壳猜;如果該對(duì)象在finalize方法中沒(méi)有與任何一個(gè)對(duì)象進(jìn)行關(guān)聯(lián)操作勾徽,那么該對(duì)象會(huì)被虛擬機(jī)進(jìn)行第二次標(biāo)記,該對(duì)象就會(huì)被垃圾回收系統(tǒng)回收统扳。值得注意的是finaliza方法JVM系統(tǒng)只會(huì)自動(dòng)調(diào)用一次喘帚,如果對(duì)象面臨下一次回收,它的finalize方法不會(huì)被再次執(zhí)行咒钟。
六吹由、再探GC實(shí)現(xiàn)機(jī)制-垃圾收集算法
(1)、標(biāo)記-清楚算法(Mark-Sweep)
? ? ? ? ? ? ? ? 用在老生代中朱嘴, 先對(duì)對(duì)象進(jìn)行標(biāo)記倾鲫,然后清楚。標(biāo)記過(guò)程就是第五部分提到的標(biāo)記過(guò)程萍嬉。值得注意的是乌昔,使用該算法清楚過(guò)后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí)壤追,無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作磕道。
(2)、復(fù)制算法(Copying)
? ? ? ? ? ? ? ? ?用在新生代中行冰,它將可用內(nèi)存按容量劃分為大小相等的兩塊溺蕉,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了悼做,就將還存活的對(duì)象復(fù)制到另外一塊上疯特,然后再把已使用過(guò)的內(nèi)存空間一次清理掉。這樣使得每次都是對(duì)整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收贿堰,內(nèi)存分配時(shí)也就不用考慮內(nèi)存碎片等復(fù)雜情況辙芍,只要移動(dòng)堆頂指針,按順序分配內(nèi)存即可羹与。
七故硅、空間分配擔(dān)保策略-GC過(guò)程中的內(nèi)存擔(dān)保機(jī)制
? ? ? 當(dāng)出現(xiàn)大量對(duì)象在Minor GC后仍然存活的情況,就需要老年代進(jìn)行分配擔(dān)保纵搁,把Survivor無(wú)法容納的對(duì)象直接進(jìn)入老年代吃衅。與生活中的銀行貸款類(lèi)似,老年代要進(jìn)行這樣的擔(dān)保腾誉,前提是老年代本身還有容納這些對(duì)象的剩余空間徘层,一共有多少對(duì)象會(huì)存活下來(lái)在實(shí)際完后才能內(nèi)存回收之前是無(wú)法明確知道的峻呕,所以只好取之前每一次回收晉升到老年代對(duì)象容量的平均大小值作為經(jīng)驗(yàn)值,與老年代的剩余空間進(jìn)行比較趣效,決定是否進(jìn)行Full GC來(lái)讓老年代騰出更多空間瘦癌。如果出現(xiàn)擔(dān)保失敗,就只好重新發(fā)起一次Full GC來(lái)進(jìn)行內(nèi)存的分配跷敬。