2.9 JVM內(nèi)存管理
2.9.1 運(yùn)行時(shí)數(shù)據(jù)區(qū)域
JVM所管理的內(nèi)存可以分為一下幾個(gè)運(yùn)行時(shí)數(shù)據(jù)區(qū)域:
![](../img/2-9-jre-data-mem.png)
其中方法區(qū)和堆是線程共享區(qū)敲街,而虛擬機(jī)棧、本地方法棧和程序計(jì)數(shù)器是線程獨(dú)占區(qū)。
程序計(jì)數(shù)器
程序計(jì)數(shù)器是線程獨(dú)有的糙申,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。執(zhí)行引擎就是通過(guò)改變這個(gè)計(jì)數(shù)器的值來(lái)選取下一條需要執(zhí)行的字節(jié)碼指令,分支玄组、循環(huán)、跳轉(zhuǎn)柜与、異常處理和線程恢復(fù)等都需要以來(lái)這個(gè)計(jì)數(shù)器來(lái)完成巧勤。當(dāng)線程執(zhí)行Java方法時(shí),這個(gè)計(jì)數(shù)器記錄的就是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址弄匕,如果是native方法颅悉,則這個(gè)計(jì)數(shù)器值為空。
虛擬機(jī)棧
一個(gè)線程對(duì)應(yīng)一個(gè)虛擬機(jī)棧迁匠,其描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀用于存儲(chǔ)局部變量表剩瓶、操作數(shù)棧驹溃、動(dòng)態(tài)連接和方法出口等信息。每一個(gè)方法從調(diào)用直到執(zhí)行完成的過(guò)程延曙,就對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧中入棧到出棧的過(guò)程豌鹤。
一般會(huì)把Java內(nèi)存簡(jiǎn)單分為堆和棧,是因?yàn)榕c對(duì)象內(nèi)存分配關(guān)系最密切的內(nèi)存區(qū)域就是這兩塊枝缔,其中“棽几恚”就是虛擬機(jī)棧,或者說(shuō)是虛擬機(jī)棧中局部變量表愿卸,之后我們會(huì)詳述“堆”灵临,。
本地方法棧
本地方法棧與虛擬機(jī)棧非常相似:區(qū)別在于虛擬機(jī)棧是虛擬機(jī)執(zhí)行Java方法趴荸,而本地方法棧是虛擬機(jī)執(zhí)行native方法儒溉。
Java堆
Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建发钝。該區(qū)域的作用是存放對(duì)象實(shí)例顿涣,幾乎所有的對(duì)象實(shí)例(數(shù)組也是對(duì)象)都在這里分配內(nèi)存。Java堆是垃圾收集器管理的主要區(qū)域酝豪。
方法區(qū)
方法區(qū)和Java堆一樣涛碑,是被所有線程共享的一塊內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類(lèi)信息寓调、常量锌唾、靜態(tài)變量等數(shù)據(jù)。該區(qū)域也會(huì)進(jìn)行垃圾回收夺英,垃圾回收的目標(biāo)主要是針對(duì)常量池的回收和對(duì)類(lèi)型的卸載晌涕。
運(yùn)行時(shí)常量池是方法區(qū)的一部分,Class文件中除了有字段痛悯、方法余黎、接口等描述信息外,還有一項(xiàng)信息就是常量池载萌,用于存放編譯期生成的各種字面量和符號(hào)引用惧财,這部分內(nèi)容將在類(lèi)加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。需要注意的是運(yùn)行時(shí)常量池具有動(dòng)態(tài)性扭仁,Java語(yǔ)言并不要求常量一定只有編譯期才能產(chǎn)生垮衷,也就是并非Class文件常量池的內(nèi)容才能進(jìn)入方法區(qū)運(yùn)行時(shí)常量池,運(yùn)行期間也可以將新的常量放入池中乖坠,比如String類(lèi)的intern方法搀突。
2.9.2 Hotspot虛擬機(jī)對(duì)象探秘
本節(jié)以Hotspot虛擬機(jī)和Java堆為例,簡(jiǎn)單探討Hotspot虛擬機(jī)在Java堆中對(duì)象分配熊泵、布局和訪問(wèn)的全過(guò)程仰迁。
對(duì)象創(chuàng)建
語(yǔ)言層面上甸昏,創(chuàng)建對(duì)象(clone、反序列化)通常僅僅是一個(gè)new關(guān)鍵字而已徐许,而在虛擬機(jī)中施蜜,對(duì)象(普通Java對(duì)象,不包括數(shù)組和Class對(duì)象等)創(chuàng)建是怎樣一個(gè)過(guò)程呢雌隅?
當(dāng)虛擬機(jī)遇到一條new指令時(shí)翻默,首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類(lèi)的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類(lèi)是否已被加載澄步、解析和初始化過(guò)冰蘑。如果沒(méi)有,那必須先執(zhí)行相應(yīng)的類(lèi)加載過(guò)程村缸。
當(dāng)類(lèi)加載檢查完成后,接下來(lái)虛擬機(jī)將為新生對(duì)象分配內(nèi)存武氓,對(duì)象所需內(nèi)存空間的大小在類(lèi)加載完成后就完全確定的梯皿。內(nèi)存分配完畢后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零值县恕。接下來(lái)虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置东羹,例如這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例,如何才能找到類(lèi)的元數(shù)據(jù)信息忠烛,對(duì)象的GC分代年齡等信息属提,這些信息存放在對(duì)象的對(duì)象頭之中。
完成上面工作后美尸,從虛擬機(jī)的角度看冤议,一個(gè)新的對(duì)象已經(jīng)產(chǎn)生了,但從Java程序的角度看师坎,對(duì)象創(chuàng)建才剛剛開(kāi)始--<init>方法(構(gòu)造方法)還沒(méi)有執(zhí)行恕酸,所有的字段都是零值。所以胯陋,一般執(zhí)行new指令之后會(huì)接著執(zhí)行<init>方法蕊温,把對(duì)象按照程序員的意愿進(jìn)行初始化,這樣一個(gè)真正可用的對(duì)象才算完全產(chǎn)生出來(lái)遏乔。
對(duì)象內(nèi)存布局
在Hotspot虛擬機(jī)中义矛,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為3塊區(qū)域:對(duì)象頭、實(shí)例數(shù)據(jù)和對(duì)齊填充盟萨。
Hotspot虛擬機(jī)的對(duì)象頭包括兩部分信息:第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)凉翻,如哈希碼、GC分代年齡鸯旁、鎖狀態(tài)標(biāo)志噪矛、線程持有的鎖量蕊、偏向線程ID、偏向時(shí)間戳等艇挨;另外一部分是類(lèi)型指針残炮,即對(duì)象指向它的類(lèi)元數(shù)據(jù)的指針,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類(lèi)的實(shí)例缩滨。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類(lèi)型指針势就,即查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過(guò)對(duì)象本身。另外脉漏,如果對(duì)象是一個(gè)數(shù)組苞冯,那么在對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過(guò)普通Java對(duì)象的元數(shù)據(jù)信息確定Java對(duì)象的大小侧巨,但是無(wú)法從數(shù)組的元數(shù)據(jù)中確定數(shù)組的大小舅锄。
接下來(lái)的實(shí)例數(shù)據(jù)部分是對(duì)象真正存儲(chǔ)的有效信息,也就是在程序代碼中所定義的各種類(lèi)型的字段內(nèi)容司忱,無(wú)論是從父類(lèi)繼承下來(lái)的皇忿,還是在子類(lèi)中定義的,都需要記錄下來(lái)坦仍。這部分的存儲(chǔ)順序會(huì)受到虛擬機(jī)分配策略和字段在Java源碼中定義順序的影響鳍烁。
對(duì)象訪問(wèn)定位
建立對(duì)象是為了使用對(duì)象,Java程序需要通過(guò)棧上的reference數(shù)據(jù)來(lái)操作堆上的具體對(duì)象繁扎。由于reference類(lèi)型在JVM規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊冕;模](méi)有定義這個(gè)引用應(yīng)該通過(guò)何種方式去定位、訪問(wèn)堆中對(duì)象的具體位置梳玫,所以對(duì)象訪問(wèn)方式也是取決于虛擬機(jī)實(shí)現(xiàn)爹梁,目前主流的訪問(wèn)方式有使用句柄和直接指針兩種。
-
句柄訪問(wèn):Java堆中將會(huì)劃分出一塊內(nèi)存來(lái)作為句柄池汽纠,reference中存儲(chǔ)的就是對(duì)象的句柄地址卫键,而句柄中包含了對(duì)象實(shí)例數(shù)據(jù)和類(lèi)型數(shù)據(jù)各自的地址信息,如下圖所示:
-
直接指針訪問(wèn):Java堆對(duì)象的布局中就必須考慮如何設(shè)置訪問(wèn)類(lèi)型數(shù)據(jù)的相關(guān)信息虱朵,而reference中存儲(chǔ)的就是對(duì)象地址莉炉。
這兩種訪問(wèn)方式各有優(yōu)劣,使用句柄訪問(wèn)的好處是reference中存儲(chǔ)的是穩(wěn)定的句柄地址碴犬,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是很普遍的行為)時(shí)只需要改變句柄中的實(shí)例數(shù)據(jù)指針絮宁,而reference本身不需要改變;而使用指針直接訪問(wèn)的好處是速度更快服协。Hotspot中采用的是直接指針訪問(wèn)绍昂。
2.9.3 垃圾收集器
上一節(jié)介紹了Java內(nèi)存運(yùn)行時(shí)區(qū)域的各個(gè)部分,其中程序計(jì)數(shù)器、虛擬機(jī)棧和本地方法棧是線程獨(dú)有的窘游,棧中的棧幀隨著方法的進(jìn)入和退出而入棧和出棧唠椭,每個(gè)棧幀分配多少內(nèi)存也是類(lèi)結(jié)構(gòu)確定后就已知的,因此這幾個(gè)區(qū)域的內(nèi)存分配和回收具有確定性:方法退出或者線程結(jié)束忍饰,內(nèi)存自然就回收了贪嫂。但是Java堆和方法區(qū)則不一樣,只有在程序處于運(yùn)行期間才能知道會(huì)創(chuàng)建哪些對(duì)象艾蓝,這部分內(nèi)存的分配和回收都是動(dòng)態(tài)的力崇,垃圾收集器關(guān)注的就是這部分內(nèi)存。
對(duì)象生命周期
垃圾收集器在對(duì)Java堆進(jìn)行垃圾回收之前赢织,第一件事情就是確定Java堆上的對(duì)象哪些是“活的”亮靴,哪些是“死的”(沒(méi)有被任何對(duì)象使用的對(duì)象),一般有以下兩種方法來(lái)判斷對(duì)象是否存活:引用計(jì)數(shù)法和可達(dá)性分析法:
- 引用計(jì)數(shù)法:給對(duì)象添加一個(gè)引用計(jì)數(shù)器于置,每當(dāng)有一個(gè)地方引用它時(shí)茧吊,計(jì)數(shù)器值加1;當(dāng)引用失效時(shí)八毯,計(jì)數(shù)器值減1饱狂;任何時(shí)刻當(dāng)計(jì)數(shù)器為0的對(duì)象就是不可能被引用到的。該方法實(shí)現(xiàn)簡(jiǎn)單宪彩,判定效率也很高,但是它的主要問(wèn)題是無(wú)法解決對(duì)象之間循環(huán)引用的問(wèn)題讲婚,所以主流的JVM都不采用該方法尿孔。
- 可達(dá)性分析:通過(guò)一系列的“GC Roots”的對(duì)象作為起始點(diǎn),從這些節(jié)點(diǎn)開(kāi)始向下搜索筹麸,搜索所走過(guò)的路徑稱為引用鏈活合,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連(使用圖論的話來(lái)說(shuō),就是從GC Roots到這個(gè)對(duì)象不可達(dá))時(shí)物赶,則證明這個(gè)對(duì)象是不可用的白指。在Java中,可以作為GC Roots的對(duì)象一般包括以下幾種:虛擬機(jī)棧(棧幀中的局部變量表)中引用的對(duì)象酵紫,方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象告嘲,方法區(qū)中常量引用的對(duì)象,本地方法棧中JNI引用的對(duì)象奖地。
再談引用
為了更好地進(jìn)行垃圾回收橄唬,Java對(duì)引用的概念進(jìn)行了擴(kuò)展,將引用分為強(qiáng)引用参歹,軟引用(SoftReference)仰楚,弱引用(WeakReference)、虛引用(PhantomReference)。這四種引用強(qiáng)度依次逐漸減弱:
- 強(qiáng)引用
- 軟引用:用來(lái)描述一些還有用但并非必需的對(duì)象僧界,在系統(tǒng)將要拋出OOM之前侨嘀,將會(huì)把這些對(duì)象列進(jìn)回收范圍之中進(jìn)行第二次回收,如果這次回收還沒(méi)有足夠的內(nèi)存捂襟,才會(huì)拋出OOM
- 弱引用:用來(lái)描述非必需對(duì)象咬腕,被弱引用關(guān)聯(lián)的對(duì)象只能活到下一次垃圾收集發(fā)生之前
- 虛引用:最弱的引用,虛引用不會(huì)影響到對(duì)象的生存時(shí)間笆豁,也無(wú)法通過(guò)虛引用來(lái)獲取一個(gè)對(duì)象實(shí)例郎汪。為對(duì)象設(shè)置虛引用關(guān)聯(lián)的唯一目的就是這個(gè)對(duì)象在被垃圾收集器回收時(shí)收到一個(gè)系統(tǒng)通知。
垃圾收集算法
本節(jié)簡(jiǎn)單討論幾種垃圾收集算法的思想:
- 標(biāo)記-清除算法闯狱,該算法分為標(biāo)記和清除兩個(gè)階段:首先標(biāo)記出所有需要回收的對(duì)象煞赢,在標(biāo)記完成后統(tǒng)一回收所有被標(biāo)記的對(duì)象。該算法比較簡(jiǎn)單哄孤,它的主要問(wèn)題有兩個(gè):一個(gè)是效率問(wèn)題照筑,標(biāo)記和清除兩個(gè)過(guò)程的效率都不高,另一個(gè)問(wèn)題是空間問(wèn)題瘦陈,標(biāo)記清楚之后會(huì)產(chǎn)生大量不連續(xù)的內(nèi)存碎片凝危。
- 復(fù)制收集算法,該算法將可用內(nèi)存分為大小相等的兩塊晨逝,每次只使用其中的一塊蛾默,當(dāng)這一塊內(nèi)存用完后,就將還存活的對(duì)象復(fù)制到另外一塊捉貌,然后再把已使用過(guò)的內(nèi)存空間清理掉支鸡。該方法實(shí)現(xiàn)簡(jiǎn)單,運(yùn)行高效趁窃,不會(huì)存在內(nèi)存碎片問(wèn)題牧挣。但是它的主要問(wèn)題是空間效率太低。現(xiàn)代JVM一般都采用該算法回收新生代醒陆,研究表明瀑构,新生代中98%的對(duì)象是“朝生暮死”。具體使用時(shí)并不需要按照1:1來(lái)劃分內(nèi)存空間刨摩,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間寺晌,每次使用Eden和一塊Survivor空間,當(dāng)回收時(shí)將Eden和Survivor還存活的對(duì)象一次性地復(fù)制到另外一塊Survivor空間上码邻,最后清理掉Eden和剛才用過(guò)的Survivor空間折剃。Hotspot默認(rèn)Eden和Survivor的大小比例是8:1投蝉,當(dāng)Survivor空間不足時(shí)裹唆,需要依賴其他內(nèi)存(老年代)進(jìn)行分配擔(dān)保佑惠。
- 標(biāo)記-整理算法,復(fù)制收集算法在對(duì)象存活率較高時(shí)就要進(jìn)行較多的復(fù)制操作心傀,效率就會(huì)降低却特,而且需要更多空間進(jìn)行分配擔(dān)保建峭。所以老年代一般不使用該算法症副,而是采用標(biāo)記-整理算法:標(biāo)記過(guò)程和標(biāo)記-清除算法一樣,但是后續(xù)步驟不是直接對(duì)可回收對(duì)象進(jìn)行清理阵子,而是讓所有存活的對(duì)象都向一端移動(dòng)思杯,然后直接清理掉端邊界以外的內(nèi)存。
- 分代收集算法挠进,現(xiàn)代JVM一般都采用分代收集算法色乾,根據(jù)對(duì)象存活周期的不同將內(nèi)存劃分為幾塊,一般分為新生代和老年代领突,新生代對(duì)象存活率低暖璧,使用復(fù)制收集算法,老年代對(duì)象存活率高君旦,使用標(biāo)記-清除或者標(biāo)記-整理算法澎办。
2.9.4 內(nèi)存分配策略
Java的自動(dòng)內(nèi)存管理主要包括兩個(gè)方面:給對(duì)象分配內(nèi)存以及回收分配給對(duì)象的內(nèi)存。上面我們已經(jīng)詳細(xì)談?wù)摿薐VM如何回收內(nèi)存金砍,接下來(lái)我們將講解幾條最普遍的內(nèi)存分配規(guī)則:
- 對(duì)象優(yōu)先在Eden區(qū)域分配局蚀,大多數(shù)情況下,對(duì)象在新生代Eden區(qū)域中分配恕稠,當(dāng)Eden區(qū)域中沒(méi)有足夠空間時(shí)琅绅,虛擬機(jī)將發(fā)起一次MinorGC。
- 大對(duì)象直接進(jìn)入老年代鹅巍,大對(duì)象就是需要大量連續(xù)內(nèi)存空間的Java對(duì)象奉件,如很長(zhǎng)的字符串和數(shù)組等。大對(duì)象對(duì)于虛擬機(jī)的內(nèi)存分配是一個(gè)壞消息(更壞的是“朝生暮死”的大對(duì)象昆著,程序中應(yīng)盡量避免),經(jīng)常出現(xiàn)大對(duì)象容易導(dǎo)致內(nèi)存還有不少空間時(shí)就提前觸發(fā)垃圾收集以獲取足夠的連續(xù)空間术陶。
- 長(zhǎng)期存活的對(duì)象進(jìn)入老年代凑懂,虛擬機(jī)采用分代收集的思想管理內(nèi)存,那么內(nèi)存分配時(shí)就必須能識(shí)別哪些對(duì)象應(yīng)該放在新生代梧宫,哪些放在老年代接谨。為了做到這點(diǎn),虛擬機(jī)給每個(gè)對(duì)象定義一個(gè)對(duì)象年齡計(jì)數(shù)器塘匣,如果對(duì)象在Eden區(qū)域出生并經(jīng)過(guò)第一次MinorGC后仍然存活脓豪,并被移動(dòng)到Survivor區(qū)域,則對(duì)象年齡設(shè)為1,對(duì)象在Survivor區(qū)中每“熬過(guò)”一次MinorGC忌卤,年齡就增加1歲扫夜,當(dāng)它的年齡增加到MaxTenuringThreshold(默認(rèn)為15歲),就會(huì)被晉升到老年代。
- 動(dòng)態(tài)對(duì)象年齡判定笤闯,為了更好的適應(yīng)不同程序的內(nèi)存狀況堕阔,虛擬機(jī)并不是一定要求對(duì)象年齡到達(dá)MaxTenuringThreshold時(shí)才能晉升到老年代,如果在Survivor區(qū)中相同年齡的對(duì)象大小總和大于Survivor空間的一半颗味,年齡大于等于該年齡的對(duì)象就可以直接進(jìn)入老年代超陆。