HostSpot虛擬機(jī)運(yùn)行時(shí)內(nèi)存
程序計(jì)數(shù)器——當(dāng)前線(xiàn)程執(zhí)行字節(jié)碼的行號(hào)指示器掂器,如果執(zhí)行Native方法,則計(jì)數(shù)器值為空俱箱,是唯一一個(gè)在Java虛擬機(jī)規(guī)范沒(méi)有規(guī)定OOM情況的區(qū)域国瓮。
Java虛擬機(jī)棧——Java方法執(zhí)行的內(nèi)存模型,每個(gè)方法以棧幀為單位乃摹,棧幀存儲(chǔ)方法的局部變量表禁漓、動(dòng)態(tài)鏈接、方法出口孵睬。每個(gè)方法調(diào)用到執(zhí)行完畢對(duì)應(yīng)一個(gè)棧幀在Java虛擬機(jī)棧入棧到出棧播歼。局部變量表除了基本類(lèi)型外還存有對(duì)象引用類(lèi)型,對(duì)象引用類(lèi)型指向一個(gè)代表對(duì)象的句柄或與其對(duì)象相關(guān)的位置掰读。
本地方法椕啬——在Sun HotSpot中本地方法棧與虛擬機(jī)棧合二為一,區(qū)別是Java虛擬機(jī)棧為執(zhí)行Java方法提供服務(wù)蹈集,本地方法棧為執(zhí)行Native方法提供服務(wù)烁试。
Java堆——所有線(xiàn)程共享的一塊區(qū)域,用于存放對(duì)象實(shí)例雾狈,也是運(yùn)行時(shí)內(nèi)存中占用空間最大的一塊廓潜,也是垃圾回收的主要區(qū)域抵皱。Java堆是垃圾收集器管理的主要區(qū)域善榛,因此很多時(shí)候也被稱(chēng)做“GC堆”。從內(nèi)存回收的角度來(lái)看呻畸,由于現(xiàn)在收集器基本都采用分代收集算法移盆,所以Java堆中還可以細(xì)分為:新生代和老年代;再細(xì)致一點(diǎn)的有Eden空間伤为、From Survivor空間咒循、To Survivor空間等。
方法區(qū)——存儲(chǔ)加載的類(lèi)信息(版本號(hào)绞愚、字段叙甸、方法、接口)位衩、常量池(如運(yùn)行時(shí)常量池)裆蒸、靜態(tài)變量、即時(shí)編譯器編譯后的代碼糖驴。
直接內(nèi)存——不屬于Java虛擬機(jī)定義的范疇僚祷,Native方法創(chuàng)建的對(duì)象儲(chǔ)存在此區(qū)域,與本機(jī)總內(nèi)存相關(guān)贮缕。
Java對(duì)象的創(chuàng)建
虛擬機(jī)遇到一條new指令辙谜,首先去方法區(qū)檢查指令的參數(shù)(類(lèi)的類(lèi)型)能否在常量池(方法區(qū))定位到類(lèi)的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已被加載感昼、解析和初始化過(guò)装哆。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程。
在類(lèi)加載檢查通過(guò)后烂琴,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存爹殊。對(duì)象所需內(nèi)存的大小在類(lèi)加載完成后便可完全確定,為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從Java堆中劃分出來(lái)奸绷。
內(nèi)存分配完成后梗夸,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值(不包括對(duì)象頭)。接下來(lái)号醉,虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置反症,例如這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例、如何才能找到類(lèi)的元數(shù)據(jù)信息畔派、對(duì)象的哈希碼铅碍、對(duì)象的GC分代年齡等信息。這些信息存放在對(duì)象的對(duì)象頭(Object Header)之中线椰。
Java對(duì)象的內(nèi)存布局
在HotSpot虛擬機(jī)中胞谈,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對(duì)齊填充(Padding)憨愉。
HotSpot虛擬機(jī)的對(duì)象頭包括兩部分信息烦绳,第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼(HashCode)配紫、GC分代年齡径密、鎖狀態(tài)標(biāo)志、線(xiàn)程持有的鎖躺孝、偏向線(xiàn)程ID享扔、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長(zhǎng)度在32位和64位的虛擬機(jī)(未開(kāi)啟壓縮指針)中分別為32bit和64bit植袍,官方稱(chēng)它為“Mark Word”惧眠。
對(duì)象頭的另外一部分是類(lèi)型指針,即對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針于个,HotSpot虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例氛魁。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類(lèi)型
指針。如果對(duì)象是一個(gè)Java數(shù)組览濒,那在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù)呆盖,因?yàn)樘摂M機(jī)可以通過(guò)普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小,但是從數(shù)組的元數(shù)據(jù)中卻無(wú)法確定數(shù)組的大小贷笛。
接下來(lái)的實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息应又,也是在程序代碼中所定義的各種類(lèi)型的字段內(nèi)容。無(wú)論是從父類(lèi)繼承下來(lái)的乏苦,還是在子類(lèi)中定義的株扛,都需要記錄起來(lái)尤筐。
第三部分對(duì)齊填充并不是必然存在的,也沒(méi)有特別的含義洞就,它僅僅起著占位符的作用盆繁。由于HotSpot VM的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是8字節(jié)的整數(shù)倍,換句話(huà)說(shuō)旬蟋,就是對(duì)象的大小必須是8字節(jié)的整數(shù)倍油昂。而對(duì)象頭部分正好是8字節(jié)的倍數(shù)(1倍或者2倍),因此倾贰,當(dāng)對(duì)象實(shí)例數(shù)據(jù)部分沒(méi)有對(duì)齊時(shí)冕碟,就需要通過(guò)對(duì)齊填充來(lái)補(bǔ)全。
Java對(duì)象的訪(fǎng)問(wèn)定位
目前主流的訪(fǎng)問(wèn)方式有使用句柄和直接指針兩種匆浙。
如果使用句柄訪(fǎng)問(wèn)的話(huà)安寺,那么Java堆中將會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池,reference中存儲(chǔ)的就是對(duì)象的句柄地址首尼,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)與類(lèi)型數(shù)據(jù)各自的具體地址信息挑庶。
如果使用直接指針訪(fǎng)問(wèn),那么Java堆對(duì)象的布局中就必須考慮如何放置訪(fǎng)問(wèn)類(lèi)型數(shù)據(jù)的相關(guān)信息软能,而reference中存儲(chǔ)的直接就是對(duì)象地址迎捺。
這兩種對(duì)象訪(fǎng)問(wèn)方式各有優(yōu)勢(shì),使用句柄來(lái)訪(fǎng)問(wèn)的最大好處就是reference中存儲(chǔ)的是穩(wěn)定的句柄地址埋嵌,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針破加,而reference本身不需要修改俱恶。
使用直接指針訪(fǎng)問(wèn)方式的最大好處就是速度更快雹嗦,它節(jié)省了一次指針定位的時(shí)間開(kāi)銷(xiāo),由于對(duì)象的訪(fǎng)問(wèn)在Java中非常頻繁合是,因此這類(lèi)開(kāi)銷(xiāo)積少成多后也是一項(xiàng)非沉俗铮可觀(guān)的執(zhí)行成本。就本書(shū)討論的主要虛擬機(jī)Sun HotSpot而言聪全,它是使用第二種方式進(jìn)行對(duì)象訪(fǎng)問(wèn)的泊藕,但從整個(gè)軟件開(kāi)發(fā)的范圍來(lái)看,各種語(yǔ)言和框架使用句柄來(lái)訪(fǎng)問(wèn)的情況也十分常見(jiàn)难礼。
如何判斷對(duì)象需要回收?
引用計(jì)數(shù)算法(不使用)
給對(duì)象中添加一個(gè)引用計(jì)數(shù)器娃圆,每當(dāng)有一個(gè)地方引用它時(shí),計(jì)數(shù)器值就加1蛾茉;當(dāng)引用失效時(shí)讼呢,計(jì)數(shù)器值就減1;任何時(shí)刻計(jì)數(shù)器為0的對(duì)象就是不可能再被使用的谦炬。
但是悦屏,至少主流的Java虛擬機(jī)里面沒(méi)有選用引用計(jì)數(shù)算法來(lái)管理內(nèi)存节沦,其中最主要的原因是它很難解決對(duì)象之間相互循環(huán)引用的問(wèn)題。如果兩個(gè)對(duì)象相互引用础爬,除此之外再無(wú)任何引用執(zhí)行這兩個(gè)對(duì)象將導(dǎo)致無(wú)法回收甫贯。
可達(dá)性分析算法(HotSpot虛擬機(jī)使用)
這個(gè)算法的基本思路就是通過(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)有任何引用鏈相連(用圖論的話(huà)來(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語(yǔ)言中,可作為GC Roots的對(duì)象包括下面幾種:
虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象蜓氨。
方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象聋袋。
方法區(qū)中常量引用的對(duì)象。
本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象穴吹。
Java中的引用類(lèi)型
在JDK 1.2以前幽勒,Java中的引用的定義很傳統(tǒng):如果reference類(lèi)型的數(shù)據(jù)中存儲(chǔ)的數(shù)值代表的是另外一塊內(nèi)存的起始地址,就稱(chēng)這塊內(nèi)存代表著一個(gè)引用港令。
在JDK 1.2之后啥容,Java對(duì)引用的概念進(jìn)行了擴(kuò)充,將引用分為強(qiáng)引用(Strong Reference)顷霹、軟引用(Soft Reference)咪惠、弱引用(Weak Reference)、虛引用(Phantom Reference)4種淋淀,這4種引用強(qiáng)度依次逐漸減弱遥昧。
強(qiáng)引用——類(lèi)似“Object obj=new Object()”這類(lèi)的引用,只要強(qiáng)引用還存在朵纷,垃圾收集器永遠(yuǎn)不會(huì)回收掉被引用的對(duì)象炭臭。
軟引用——在系統(tǒng)將要發(fā)生內(nèi)存溢出異常之前,將會(huì)把弱引用對(duì)象進(jìn)回收范圍之中進(jìn)行第二次回收袍辞。
弱引用——當(dāng)垃圾收集器工作時(shí)鞋仍,無(wú)論當(dāng)前內(nèi)存是否足夠,都會(huì)回收掉只被弱引用關(guān)聯(lián)的對(duì)象革屠。因此弱引用無(wú)法存活到下次GC之后凿试。
虛引用——一個(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)通知板甘。
finalize()方法
即使在可達(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()方法鸯檬。當(dāng)對(duì)象沒(méi)有覆蓋finalize()方法伶选,或者finalize()方法已經(jīng)被虛擬機(jī)調(diào)用過(guò)隐岛,虛擬機(jī)將這兩種情況都視為“沒(méi)有必要執(zhí)行”猫妙。
任何一個(gè)對(duì)象的finalize()方法都只會(huì)被系統(tǒng)自動(dòng)調(diào)用一次,如果對(duì)象面臨下一次回收聚凹,它的finalize()方法不會(huì)被再次執(zhí)行割坠。
需要特別說(shuō)明的是,上面關(guān)于對(duì)象死亡時(shí)finalize()方法的描述可能帶有悲情的藝術(shù)色彩妒牙,筆者并不鼓勵(lì)大家使用這種方法來(lái)拯救對(duì)象彼哼。相反,筆者建議大家盡量避免使用它湘今,因?yàn)樗皇荂/C++中的析構(gòu)函數(shù)敢朱,而是Java剛誕生時(shí)為了使C/C++程序員更容易接受它所做出的一個(gè)妥協(xié)。它的運(yùn)行代價(jià)高昂摩瞎,不確定性大拴签,無(wú)法保證各個(gè)對(duì)象的調(diào)用順序。有些教材中描述它適合做“關(guān)閉外部資源”之類(lèi)的工作愉豺,這完全是對(duì)這個(gè)方法用途的一種自我安慰篓吁。
finalize()能做的所有工作茫因,使用try-finally或者其他方式都可以做得更好蚪拦、更及時(shí),所以筆
者建議大家完全可以忘掉Java語(yǔ)言中有這個(gè)方法的存在冻押。
垃圾回收算法
標(biāo)記-清除算法
如同它的名字一樣驰贷,算法分為“標(biāo)記”和“清除”兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象洛巢。
它的主要不足有兩個(gè):一個(gè)是效率問(wèn)題括袒,標(biāo)記和清除兩個(gè)過(guò)程的效率都不高;另一個(gè)是空間問(wèn)題稿茉,標(biāo)記清除之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片锹锰,空間碎片太多可能會(huì)導(dǎo)致以后在程序運(yùn)行過(guò)程中需要分配較大對(duì)象時(shí)芥炭,無(wú)法找到足夠的連續(xù)內(nèi)存而不得不提前觸發(fā)另一次垃圾收集動(dòng)作。
標(biāo)記—清除算法的執(zhí)行過(guò)程如圖所示恃慧。
復(fù)制算法
它將可用內(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)存即可城侧,實(shí)現(xiàn)簡(jiǎn)單易遣,運(yùn)行高效。只是這種算法的代價(jià)是將內(nèi)存縮小為了原來(lái)的一半嫌佑,未免太高了一點(diǎn)训挡。
現(xiàn)在的商業(yè)虛擬機(jī)都采用這種收集算法來(lái)回收新生代,IBM公司的專(zhuān)門(mén)研究表明歧强,新生代中的對(duì)象98%是“朝生夕死”的澜薄,所以并不需要按照1:1的比例來(lái)劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間摊册,每次使用Eden和其中一塊Survivor[1]肤京。當(dāng)回收時(shí),將Eden和Survivor中還存活著的對(duì)象一次性地復(fù)制到另外一塊Survivor空間上茅特,最后清理掉Eden和剛才用過(guò)的Survivor空間忘分。HotSpot虛擬機(jī)默認(rèn)Eden和Survivor的大小比例是8:1,也就是每次新生代中可用內(nèi)存空間為整個(gè)新生代容量的90%(80%+10%)白修,只有10%的內(nèi)存會(huì)被“浪費(fèi)”妒峦。當(dāng)然,98%的對(duì)象可回收只是一般場(chǎng)景下的數(shù)據(jù)兵睛,我們沒(méi)有辦法保證每次回收都只有不多于10%的對(duì)象存活肯骇,當(dāng)Survivor空間不夠用時(shí),需要依賴(lài)其他內(nèi)存(這里指老年代)進(jìn)行分配擔(dān)保(Handle romotion)祖很。
標(biāo)記-整理算法
標(biāo)記過(guò)程仍然與“標(biāo)記-清除”算法一樣笛丙,但后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理,而是讓所有存活的對(duì)象都向一端移動(dòng)假颇,然后直接清理掉端邊界以外的內(nèi)存胚鸯。
分代收集算法
當(dāng)前商業(yè)虛擬機(jī)的垃圾收集都采用“分代收集”(Generational Collection)算法,這種算法并沒(méi)有什么新的思想笨鸡,只是根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊姜钳。一般是把Java堆分為新生代和老年代坦冠,這樣就可以根據(jù)各個(gè)年代的特點(diǎn)采用最適當(dāng)?shù)氖占惴āT谛律懈缜牛看卫占瘯r(shí)都發(fā)現(xiàn)有大批對(duì)象死去蓝牲,只有少量存活,那就選用復(fù)制算法泰讽,只需要付出少量存活對(duì)象的復(fù)制成本就可以完成收集例衍。而老年代中因?yàn)閷?duì)象存活率高、沒(méi)有額外空間對(duì)它進(jìn)行分配擔(dān)保已卸,就必須使用“標(biāo)記—清理”或者“標(biāo)記—整理”算法來(lái)進(jìn)行回收佛玄。
HotSpot的算法實(shí)現(xiàn)
枚舉根節(jié)點(diǎn)
在HotSpot的實(shí)現(xiàn)中,是使用一組稱(chēng)為OopMap的數(shù)據(jù)結(jié)構(gòu)來(lái)得知哪些地方存放著對(duì)象引用累澡,在類(lèi)加載完成的時(shí)候梦抢,HotSpot就把對(duì)象內(nèi)什么偏移量上是什么類(lèi)型的數(shù)據(jù)計(jì)算出來(lái),在JIT編譯過(guò)程中愧哟,也
會(huì)在特定的位置記錄下棧和寄存器中哪些位置是引用奥吩。這樣,GC在掃描時(shí)就可以直接得知這些信息了蕊梧。
安全點(diǎn)
實(shí)際上霞赫,HotSpot也的確沒(méi)有為每條指令都生成OopMap,前面已經(jīng)提到肥矢,只是在“特定的位置”記錄了這些信息端衰,這些位置稱(chēng)為安全點(diǎn)(Safepoint),即程序執(zhí)行時(shí)并非在所有地方都能停頓下來(lái)開(kāi)始GC甘改,只有在到達(dá)安全點(diǎn)時(shí)才能暫停旅东。Safepoint的選定既不能太少以致于讓GC等待時(shí)間太長(zhǎng),也不能過(guò)于頻繁以致于過(guò)分增大運(yùn)行時(shí)的負(fù)荷十艾。所以抵代,安全點(diǎn)的選定基本上是以程序“是否具有讓程序長(zhǎng)時(shí)間執(zhí)行的特征”為標(biāo)準(zhǔn)進(jìn)行選定的——因?yàn)槊織l指令執(zhí)行的時(shí)間都非常短暫,程序不太可能因?yàn)橹噶盍鏖L(zhǎng)度太長(zhǎng)這個(gè)原因而過(guò)長(zhǎng)時(shí)間運(yùn)行忘嫉,“長(zhǎng)時(shí)間執(zhí)行”的最明顯特征就是指令序列復(fù)用荤牍,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)榄融、異常跳轉(zhuǎn)等参淫,所以具有這些功能的指令才會(huì)產(chǎn)生Safepoint。
對(duì)于Sefepoint愧杯,另一個(gè)需要考慮的問(wèn)題是如何在GC發(fā)生時(shí)讓所有線(xiàn)程(這里不包括執(zhí)行JNI調(diào)用的線(xiàn)程)都“跑”到最近的安全點(diǎn)上再停頓下來(lái)。這里有兩種方案可供選擇:搶先式中斷(Preemptive Suspension)和主動(dòng)式中斷(Voluntary Suspension)鞋既,其中搶先式中斷不需要線(xiàn)程的執(zhí)行代碼主動(dòng)去配合力九,在GC發(fā)生時(shí)耍铜,首先把所有線(xiàn)程全部中斷,如果發(fā)現(xiàn)有線(xiàn)程中斷的地方不在安全點(diǎn)上跌前,就恢復(fù)線(xiàn)程棕兼,讓它“跑”到安全點(diǎn)上。現(xiàn)在幾乎沒(méi)有虛擬機(jī)實(shí)現(xiàn)采用搶先式中斷來(lái)暫停線(xiàn)程從而響應(yīng)GC事件抵乓。
而主動(dòng)式中斷的思想是當(dāng)GC需要中斷線(xiàn)程的時(shí)候伴挚,不直接對(duì)線(xiàn)程操作,僅僅簡(jiǎn)單地設(shè)置一個(gè)標(biāo)志灾炭,各個(gè)線(xiàn)程執(zhí)行時(shí)主動(dòng)去輪詢(xún)這個(gè)標(biāo)志茎芋,發(fā)現(xiàn)中斷標(biāo)志為真時(shí)就自己中斷掛起。輪詢(xún)標(biāo)志的地方和安全點(diǎn)是重合的蜈出,另外再加上創(chuàng)建對(duì)象需要分配內(nèi)存的地方田弥。
安全區(qū)域
使用Safepoint似乎已經(jīng)完美地解決了如何進(jìn)入GC的問(wèn)題,但實(shí)際情況卻并不一定铡原。Safepoint機(jī)制保證了程序執(zhí)行時(shí)偷厦,在不太長(zhǎng)的時(shí)間內(nèi)就會(huì)遇到可進(jìn)入GC的Safepoint。但是燕刻,程序“不執(zhí)行”的時(shí)候呢只泼?所謂的程序不執(zhí)行就是沒(méi)有分配CPU時(shí)間,典型的例子就是線(xiàn)程處于Sleep狀態(tài)或者Blocked狀態(tài)卵洗,這時(shí)候線(xiàn)程無(wú)法響應(yīng)JVM的中斷請(qǐng)求辜妓,“走”到安全的地方去中斷掛起,JVM也顯然不太可能等待線(xiàn)程重新被分配CPU時(shí)間忌怎。對(duì)于這種情況籍滴,就需要安全區(qū)域(Safe Region)來(lái)解決。
安全區(qū)域是指在一段代碼片段之中榴啸,引用關(guān)系不會(huì)發(fā)生變化孽惰。在這個(gè)區(qū)域中的任意地方開(kāi)始GC都是安全的。我們也可以把Safe Region看做是被擴(kuò)展了的Safepoint鸥印。
在線(xiàn)程執(zhí)行到Safe Region中的代碼時(shí)勋功,首先標(biāo)識(shí)自己已經(jīng)進(jìn)入了Safe Region,那樣库说,當(dāng)在這段時(shí)間里JVM要發(fā)起GC時(shí)狂鞋,就不用管標(biāo)識(shí)自己為Safe Region狀態(tài)的線(xiàn)程了。在線(xiàn)程要離開(kāi)Safe Region時(shí)潜的,它要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者是整個(gè)GC過(guò)程)骚揍,如果完成了,那線(xiàn)程就繼續(xù)執(zhí)行,否則它就必須等待直到收到可以安全離開(kāi)Safe Region的信號(hào)為止信不。
垃圾收集器
如果說(shuō)收集算法是內(nèi)存回收的方法論嘲叔,那么垃圾收集器就是內(nèi)存回收的具體實(shí)現(xiàn)。Java虛擬機(jī)規(guī)范中對(duì)垃圾收集器應(yīng)該如何實(shí)現(xiàn)并沒(méi)有任何規(guī)定抽活,因此不同的廠(chǎng)商硫戈、不同版本的虛擬機(jī)所提供的垃圾收集器都可能會(huì)有很大差別。
如果兩個(gè)收集器之間存在連線(xiàn)下硕,就說(shuō)明它們可以搭配使用丁逝。虛擬機(jī)所處的區(qū)域,則表示它是屬于新生代收集器還是老年代收集器梭姓。
Serial收集器
Serial收集器是最基本霜幼、發(fā)展歷史最悠久的收集器,曾經(jīng)(在JDK 1.3.1之前)是虛擬機(jī)新生代收集的唯一選擇糊昙。
這個(gè)收集器是一個(gè)單線(xiàn)程的收集器辛掠,但它的“單線(xiàn)程”的意義并不僅僅說(shuō)明它只會(huì)使用一個(gè)CPU或一條收集線(xiàn)程去完成垃圾收集工作,更重要的是在它進(jìn)行垃圾收集時(shí)释牺,必須暫停其他所有的工作線(xiàn)程萝衩,直到它收集結(jié)束。
實(shí)際上到現(xiàn)在為止没咙,它依然是虛擬機(jī)運(yùn)行在Client模式下的默認(rèn)新生代收集器猩谊。它也有著優(yōu)于其他收集器的地方:簡(jiǎn)單而高效(與其他收集器的單線(xiàn)程比),對(duì)于限定單個(gè)CPU的環(huán)境來(lái)說(shuō)祭刚,Serial收集器由于沒(méi)有線(xiàn)程交互的開(kāi)銷(xiāo)牌捷,專(zhuān)心做垃圾收集自然可以獲得最高的單線(xiàn)程收集效率。
ParNew收集器
ParNew收集器其實(shí)就是Serial收集器的多線(xiàn)程版本涡驮,除了使用多條線(xiàn)程進(jìn)行垃圾收集之外暗甥,其余行為包括Serial收集器可用的所有控制參數(shù)、收集算法捉捅、Stop The World撤防、對(duì)象分配規(guī)則、回收策略等都與Serial收集器完全一樣棒口,在實(shí)現(xiàn)上寄月,這兩種收集器也共用了相當(dāng)多的代碼。
ParNew收集器除了多線(xiàn)程收集之外无牵,其他與Serial收集器相比并沒(méi)有太多創(chuàng)新之處漾肮,但它卻是許多運(yùn)行在Server模式下的虛擬機(jī)中首選的新生代收集器,其中有一個(gè)與性能無(wú)關(guān)但很重要的原因是茎毁,除了Serial收集器外克懊,目前只有它能與CMS收集器配合工作。
ParNew收集器在單CPU的環(huán)境中絕對(duì)不會(huì)有比Serial收集器更好的效果,甚至由于存在線(xiàn)程交互的開(kāi)銷(xiāo)保檐,該收集器在通過(guò)超線(xiàn)程技術(shù)實(shí)現(xiàn)的兩個(gè)CPU的環(huán)境中都不能百分之百地保證可以超越Serial收集器耕蝉。當(dāng)然崔梗,隨著可以使用的CPU的數(shù)量的增加夜只,它對(duì)于GC時(shí)系統(tǒng)資源的有效利用還是很有好處的。它默認(rèn)開(kāi)啟的收集線(xiàn)程數(shù)與CPU的數(shù)量相同蒜魄,
并行(Parallel):指多條垃圾收集線(xiàn)程并行工作扔亥,但此時(shí)用戶(hù)線(xiàn)程仍然處于等待狀態(tài)。
并發(fā)(Concurrent):指用戶(hù)線(xiàn)程與垃圾收集線(xiàn)程同時(shí)執(zhí)行(但不一定是并行的谈为,可能會(huì)交替執(zhí)行)旅挤,用戶(hù)程序在繼續(xù)運(yùn)行,而垃圾收集程序運(yùn)行于另一個(gè)CPU上伞鲫。
Parallel Scavenge收集器
Parallel Scavenge收集器的特點(diǎn)是它的關(guān)注點(diǎn)與其他收集器不同粘茄,CMS等收集器的關(guān)注點(diǎn)是盡可能地縮短垃圾收集時(shí)用戶(hù)線(xiàn)程的停頓時(shí)間,而Parallel Scavenge收集器的目標(biāo)則是達(dá)到一個(gè)可控制的吞吐量(Throughput)秕脓。所謂吞吐量就是CPU用于運(yùn)行用戶(hù)代碼的時(shí)間與CPU總消耗時(shí)間的比值柒瓣,即吞吐量=運(yùn)行用戶(hù)代碼時(shí)間/(運(yùn)行用戶(hù)代碼時(shí)間+垃圾收集時(shí)間),虛擬機(jī)總共運(yùn)行了100分鐘吠架,其中垃圾收集花掉1分鐘芙贫,那吞吐量就是99%。
停頓時(shí)間越短就越適合需要與用戶(hù)交互的程序傍药,良好的響應(yīng)速度能提升用戶(hù)體驗(yàn)磺平,而高吞吐量則可以高效率地利用CPU時(shí)間,盡快完成程序的運(yùn)算任務(wù)拐辽,主要適合在后臺(tái)運(yùn)算而不需要太多交互的任務(wù)拣挪。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同樣是一個(gè)單線(xiàn)程收集器俱诸,使用“標(biāo)記-整理”算法菠劝。這個(gè)收集器的主要意義也是在于給Client模式下的虛擬機(jī)使用。如果在Server模式下乙埃,那么它主要還有兩大用途:一種用途是在JDK 1.5以及之前的版本中與Parallel Scavenge收集器搭配使用[1]闸英,另一種用途就是作為CMS收集器的后備預(yù)案,在并發(fā)收集發(fā)生Concurrent Mode Failure時(shí)使用介袜。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本甫何,使用多線(xiàn)程和“標(biāo)記-整理”算法。這個(gè)收集器是在JDK 1.6中才開(kāi)始提供的遇伞,在此之前辙喂,新生代的Parallel Scavenge收集器一直處于比較尷尬的狀態(tài)。原因是,如果新生代選擇了Parallel Scavenge收集器巍耗,老年代除了Serial Old(PS MarkSweep)收集器外別無(wú)選擇(還記得上面說(shuō)過(guò)Parallel Scavenge收集器無(wú)法與CMS收集器配合工作嗎秋麸?)。由于老年代Serial Old收集器在服務(wù)端應(yīng)用性能上的“拖累”炬太,使用了Parallel Scavenge收集器也未必能在整體應(yīng)用上獲得吞吐量最大化的效果灸蟆,由于單線(xiàn)程的老年代收集中無(wú)法充分利用服務(wù)器多CPU的處理能力,在老年代很大而且硬件比較高級(jí)的環(huán)境中亲族,這種組合的吞吐量甚至還不一定有ParNew加CMS的組合“給力”炒考。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時(shí)間為目標(biāo)的收集器。目前很大一部分的Java應(yīng)用集中在互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務(wù)端上霎迫,這類(lèi)應(yīng)用尤其重
視服務(wù)的響應(yīng)速度斋枢,希望系統(tǒng)停頓時(shí)間最短,以給用戶(hù)帶來(lái)較好的體驗(yàn)知给。CMS收集器就非常符合這類(lèi)應(yīng)用的需求瓤帚。
CMS收集器是基于“標(biāo)記—清除”算法實(shí)現(xiàn)的,它的運(yùn)作過(guò)程相對(duì)于前面幾種收集器來(lái)說(shuō)更復(fù)雜一些涩赢,整個(gè)過(guò)程分為4個(gè)步驟戈次,包括:
初始標(biāo)記(CMS initial mark)
并發(fā)標(biāo)記(CMS concurrent mark)
重新標(biāo)記(CMS remark)
并發(fā)清除(CMS concurrent sweep)
其中,初始標(biāo)記谒主、重新標(biāo)記這兩個(gè)步驟仍然需要“Stop The World”朝扼。初始標(biāo)記僅僅只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對(duì)象,速度很快霎肯,并發(fā)標(biāo)記階段就是進(jìn)行GC RootsTracing的過(guò)程擎颖,而重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶(hù)程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動(dòng)的那一部分對(duì)象的標(biāo)記記錄,這個(gè)階段的停頓時(shí)間一般會(huì)比初始標(biāo)記階段稍長(zhǎng)一些观游,但遠(yuǎn)比并發(fā)標(biāo)記的時(shí)間短搂捧。
由于整個(gè)過(guò)程中耗時(shí)最長(zhǎng)的并發(fā)標(biāo)記和并發(fā)清除過(guò)程收集器線(xiàn)程都可以與用戶(hù)線(xiàn)程一起工作,所以懂缕,從總體上來(lái)說(shuō)允跑,CMS收集器的內(nèi)存回收過(guò)程是與用戶(hù)線(xiàn)程一起并發(fā)執(zhí)行的。
CMS是一款優(yōu)秀的收集器搪柑,它的主要優(yōu)點(diǎn)在名字上已經(jīng)體現(xiàn)出來(lái)了:并發(fā)收集聋丝、低停頓,Sun公司的一些官方文檔中也稱(chēng)之為并發(fā)低停頓收集器(Concurrent Low Pause Collector)工碾。但是CMS還遠(yuǎn)達(dá)不到完美的程度弱睦,它有以下3個(gè)明顯的缺點(diǎn):
CMS收集器對(duì)CPU資源非常敏感。其實(shí)渊额,面向并發(fā)設(shè)計(jì)的程序都對(duì)CPU資源比較敏感况木。在并發(fā)階段垒拢,它雖然不會(huì)導(dǎo)致用戶(hù)線(xiàn)程停頓,但是會(huì)因?yàn)檎加昧艘徊糠志€(xiàn)程(或者說(shuō)CPU資源)而導(dǎo)致應(yīng)用程序變慢火惊,總吞吐量會(huì)降低求类。CMS默認(rèn)啟動(dòng)的回收線(xiàn)程數(shù)是(CPU數(shù)量+3)/4,也就是當(dāng)CPU在4個(gè)以上時(shí)屹耐,并發(fā)回收時(shí)垃圾收集線(xiàn)程不少于25%的CPU資源尸疆,并且隨著CPU數(shù)量的增加而下降。但是當(dāng)CPU不足4個(gè)(譬如2個(gè))時(shí)张症,CMS對(duì)用戶(hù)程序的影響就可能變得很大仓技,如果本來(lái)CPU負(fù)載就比較大鸵贬,還分出一半的運(yùn)算能力去執(zhí)行收集器線(xiàn)程俗他,就可能導(dǎo)致用戶(hù)程序的執(zhí)行速度忽然降低了50%,其實(shí)也讓人無(wú)法接受阔逼。
CMS收集器無(wú)法處理浮動(dòng)垃圾(Floating Garbage)兆衅,可能出現(xiàn)“Concurrent Mode Failure”失敗而導(dǎo)致另一次Full GC的產(chǎn)生。由于CMS并發(fā)清理階段用戶(hù)線(xiàn)程還在運(yùn)行著嗜浮,伴隨程序運(yùn)行自然就還會(huì)有新的垃圾不斷產(chǎn)生羡亩,這一部分垃圾出現(xiàn)在標(biāo)記過(guò)程之后,CMS無(wú)法在當(dāng)次收集中處理掉它們危融,只好留待下一次GC時(shí)再清理掉畏铆。這一部分垃圾就稱(chēng)為“浮動(dòng)垃圾”。也是由于在垃圾收集階段用戶(hù)線(xiàn)程還需要運(yùn)行吉殃,那也就還需要預(yù)留有足夠的內(nèi)存空間給用戶(hù)線(xiàn)程使用辞居,因此CMS收集器不能像其他收集器那樣等到老年代幾乎完全被填滿(mǎn)了再進(jìn)行收集,需要預(yù)留一部分空間提供并發(fā)收集時(shí)的程序運(yùn)作使用蛋勺。
CMS是一款基于“標(biāo)記—清除”算法實(shí)現(xiàn)的收集器瓦灶,這意味著收集結(jié)束時(shí)會(huì)有大量空間碎片產(chǎn)生”辏空間碎片過(guò)多時(shí)贼陶,將會(huì)給大對(duì)象分配帶來(lái)很大麻煩,往往會(huì)出現(xiàn)老年代還有很大空間剩余巧娱,但是無(wú)法找到足夠大的連續(xù)空間來(lái)分配當(dāng)前對(duì)象碉怔,不得不提前觸發(fā)一次FullGC。
G1收集器
G1(Garbage-First)收集器是當(dāng)今收集器技術(shù)發(fā)展的最前沿成果之一禁添,早在JDK 1.7剛剛確立項(xiàng)目目標(biāo)撮胧,Sun公司給出的JDK 1.7 RoadMap里面,它就被視為JDK 1.7中HotSpot虛擬機(jī)的一個(gè)重要進(jìn)化特征上荡。從JDK 6u14中開(kāi)始就有Early Access版本的G1收集器供開(kāi)發(fā)人員實(shí)驗(yàn)趴樱、試用馒闷,由此開(kāi)始G1收集器的“Experimental”狀態(tài)持續(xù)了數(shù)年時(shí)間,直至JDK 7u4叁征,Sun公司才認(rèn)為它達(dá)到足夠成熟的商用程度纳账,移除了“Experimental”的標(biāo)識(shí)。
G1是一款面向服務(wù)端應(yīng)用的垃圾收集器捺疼。HotSpot開(kāi)發(fā)團(tuán)隊(duì)賦予它的使命是(在比較長(zhǎng)期的)未來(lái)可以替換掉JDK 1.5中發(fā)布的CMS收集器疏虫。與其他GC收集器相比,G1具備如下特點(diǎn):
并行與并發(fā):G1能充分利用多CPU啤呼、多核環(huán)境下的硬件優(yōu)勢(shì)卧秘,使用多個(gè)CPU(CPU或者CPU核心)來(lái)縮短Stop-The-World停頓的時(shí)間,部分其他收集器原本需要停頓Java線(xiàn)程執(zhí)行的GC動(dòng)作官扣,G1收集器仍然可以通過(guò)并發(fā)的方式讓Java程序繼續(xù)執(zhí)行翅敌。
分代收集:與其他收集器一樣,分代概念在G1中依然得以保留惕蹄。雖然G1可以不需要其他收集器配合就能獨(dú)立管理整個(gè)GC堆蚯涮,但它能夠采用不同的方式去處理新創(chuàng)建的對(duì)象和已經(jīng)存活了一段時(shí)間、熬過(guò)多次GC的舊對(duì)象以獲取更好的收集效果卖陵。
空間整合:與CMS的“標(biāo)記—清理”算法不同遭顶,G1從整體來(lái)看是基于“標(biāo)記—整理”算法實(shí)現(xiàn)的收集器,從局部(兩個(gè)Region之間)上來(lái)看是基于“復(fù)制”算法實(shí)現(xiàn)的泪蔫,但無(wú)論如何棒旗,這兩種算法都意味著G1運(yùn)作期間不會(huì)產(chǎn)生內(nèi)存空間碎片,收集后能提供規(guī)整的可用內(nèi)存撩荣。這種特性有利于程序長(zhǎng)時(shí)間運(yùn)行铣揉,分配大對(duì)象時(shí)不會(huì)因?yàn)闊o(wú)法找到連續(xù)內(nèi)存空間而提前觸發(fā)下一次GC。
可預(yù)測(cè)的停頓:這是G1相對(duì)于CMS的另一大優(yōu)勢(shì)婿滓,降低停頓時(shí)間是G1和CMS共同的關(guān)注點(diǎn)老速,但G1除了追求低停頓外,還能建立可預(yù)測(cè)的停頓時(shí)間模型凸主,能讓使用者明確指定在一個(gè)長(zhǎng)度為M毫秒的時(shí)間片段內(nèi)橘券,消耗在垃圾收集上的時(shí)間不得超過(guò)N毫秒,這幾乎已經(jīng)是實(shí)時(shí)Java(RTSJ)的垃圾收集器的特征了卿吐。
內(nèi)存分配與回收策略
對(duì)象優(yōu)先在Eden分配(新生代)
大多數(shù)情況下旁舰,對(duì)象在新生代Eden區(qū)中分配。當(dāng)Eden區(qū)沒(méi)有足夠空間進(jìn)行分配時(shí)嗡官,虛擬機(jī)將發(fā)起一次Minor GC箭窜。
新生代GC(Minor GC):指發(fā)生在新生代的垃圾收集動(dòng)作,因?yàn)镴ava對(duì)象大多都具備朝生夕滅的特性衍腥,所以Minor GC非常頻繁磺樱,一般回收速度也比較快纳猫。
老年代GC(Major GC/Full GC):指發(fā)生在老年代的GC,出現(xiàn)了Major GC竹捉,經(jīng)常會(huì)伴隨至少一次的Minor GC(但非絕對(duì)的芜辕,在Parallel Scavenge收集器的收集策略里就有直接進(jìn)行Major GC的策略選擇過(guò)程)。Major GC的速度一般會(huì)比Minor GC慢10倍以上块差。
大對(duì)象直接進(jìn)入老年代
所謂的大對(duì)象是指侵续,需要大量連續(xù)內(nèi)存空間的Java對(duì)象,最典型的大對(duì)象就是那種很長(zhǎng)的字符串以及數(shù)組(筆者列出的例子中的byte[]數(shù)組就是典型的大對(duì)象)憨闰。大對(duì)象對(duì)虛擬機(jī)的內(nèi)存分配來(lái)說(shuō)就是一個(gè)壞消息(替Java虛擬機(jī)抱怨一句状蜗,比遇到一個(gè)大對(duì)象更加壞的消息就是遇到一群“朝生夕滅”的“短命大對(duì)象”,寫(xiě)程序的時(shí)候應(yīng)當(dāng)避免)鹉动,經(jīng)常出現(xiàn)大對(duì)象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間來(lái)“安置”它們(新生代復(fù)制算法導(dǎo)致空間整理)轧坎。
長(zhǎng)期存活的對(duì)象將進(jìn)入老年代
既然虛擬機(jī)采用了分代收集的思想來(lái)管理內(nèi)存,那么內(nèi)存回收時(shí)就必須能識(shí)別哪些對(duì)象應(yīng)放在新生代训裆,哪些對(duì)象應(yīng)放在老年代中眶根。為了做到這點(diǎn),虛擬機(jī)給每個(gè)對(duì)象定義了一個(gè)對(duì)象年齡(Age)計(jì)數(shù)器边琉。如果對(duì)象在Eden出生并經(jīng)過(guò)第一次Minor GC后仍然存活,并且能被Survivor容納的話(huà)记劝,將被移動(dòng)到Survivor空間中变姨,并且對(duì)象年齡設(shè)為1。對(duì)象在Survivor區(qū)中每“熬過(guò)”一次Minor GC厌丑,年齡就增加1歲定欧,當(dāng)它的年齡增加到一定程度(默認(rèn)為15歲),就將會(huì)被晉升到老年代中怒竿。
動(dòng)態(tài)對(duì)象年齡判定
為了能更好地適應(yīng)不同程序的內(nèi)存狀況砍鸠,虛擬機(jī)并不是永遠(yuǎn)地要求對(duì)象的年齡必須達(dá)到了MaxTenuringThreshold才能晉升老年代,如果在Survivor空間中相同年齡所有對(duì)象大小的總和大于Survivor空間的一半耕驰,年齡大于或等于該年齡的對(duì)象就可以直接進(jìn)入老年代爷辱,無(wú)須等到MaxTenuringThreshold中要求的年齡。
空間分配擔(dān)保
在發(fā)生Minor GC之前朦肘,虛擬機(jī)會(huì)先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對(duì)象總空間饭弓,如果這個(gè)條件成立,那么Minor GC可以確保是安全的媒抠。如果不成立弟断,則虛擬機(jī)會(huì)查看HandlePromotionFailure設(shè)置值是否允許擔(dān)保失敗。如果允許趴生,那么會(huì)繼續(xù)檢查老年代最大可用的連續(xù)空間是否大于歷次晉升到老年代對(duì)象的平均大小阀趴,如果大于昏翰,將嘗試著進(jìn)行一次Minor GC,盡管這次Minor GC是有風(fēng)險(xiǎn)的刘急;如果小于矩父,或者HandlePromotionFailure設(shè)置不允許冒險(xiǎn),那這時(shí)也要改為進(jìn)行一次Full GC排霉。
下面解釋一下“冒險(xiǎn)”是冒了什么風(fēng)險(xiǎn)窍株,前面提到過(guò),新生代使用復(fù)制收集算法攻柠,但為了內(nèi)存利用率球订,只使用其中一個(gè)Survivor空間來(lái)作為輪換備份,因此當(dāng)出現(xiàn)大量對(duì)象在Minor GC后仍然存活的情況(最極端的情況就是內(nèi)存回收后新生代中所有對(duì)象都存活)瑰钮,就需要老年代進(jìn)行分配擔(dān)保冒滩,把Survivor無(wú)法容納的對(duì)象直接進(jìn)入老年代。與生活中的貸款擔(dā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)讓老年代騰出更多空間凶杖。