JVM內(nèi)存布局

? ??????【文章僅供非商業(yè)用途或交流學(xué)習(xí)使用】


? ? ? ? 下圖是經(jīng)典的JVM內(nèi)存布局:

JVM內(nèi)存布局

? ? ? ? 1? Heap (堆)

? ? ? ? Heap是OOM故障最主要的發(fā)源地,它存儲(chǔ)著幾乎所有的實(shí)例對(duì)象,堆由垃圾收集器自動(dòng)回收,堆區(qū)域由各子線程共享使用。通常情況下,它所占用的空間是所有內(nèi)存區(qū)域中最大的,但如果無節(jié)制地創(chuàng)建大量對(duì)象,也容易消耗完所有的空間。堆的內(nèi)存空間既可以固定大小,也可以在運(yùn)行時(shí)動(dòng)態(tài)的調(diào)整,比如可以通過-Xms 256M來設(shè)定初始值,通過-Xmx512M來設(shè)定最大值,其中-X代表它是JVM運(yùn)行參數(shù),ms是memory start的簡(jiǎn)稱,mx是memory max的簡(jiǎn)稱,分別代表最小堆容量和最大堆容量。但是在通常情況下,服務(wù)器在運(yùn)行過程中,堆空間不斷的擴(kuò)容與回縮,會(huì)形成不必要的系統(tǒng)壓力,所以在生產(chǎn)環(huán)境中,建議JVM的Xms和Xmx設(shè)置為同樣大小,以免在GC后調(diào)整堆大小時(shí)帶來的額外壓力。

? ? ? ? 堆分為兩大部分:新生代和老年代。對(duì)象產(chǎn)生之初在新生代,步入暮年時(shí)進(jìn)入老年代,但是老年代也會(huì)接納在新生代無法容納的超大對(duì)象。新生代由1個(gè)Eden區(qū)和2個(gè)Survivor區(qū)組成,絕大部分對(duì)象在Eden區(qū)申城,當(dāng)Eden區(qū)裝填滿的時(shí)候,會(huì)觸發(fā)YGC。當(dāng)垃圾回收的時(shí)候,在Eden區(qū)實(shí)現(xiàn)清除策略,沒有被引用的對(duì)象則直接回收。依然存活的對(duì)象會(huì)被移送到Survivor區(qū)。Survivor區(qū)分為S0和S1兩塊內(nèi)存空間,每次YGC的時(shí)候,它們將存活的對(duì)象復(fù)制到未使用的那塊空間,然后將當(dāng)前正在使用的空間完全清楚,交換兩塊空間的使用狀態(tài)。如果YGC要移送的對(duì)象大于Survivor區(qū)容量的上線,則直接移交給老年代。假如一些沒有進(jìn)取心的對(duì)象以為可以一直在新生代的Survivor區(qū)交換來交換去,那就錯(cuò)了。每個(gè)對(duì)象都有一個(gè)計(jì)數(shù)器,每次YGC都會(huì)加1。-XX:MaxTenuringThreshold參數(shù)能配置計(jì)數(shù)器的值達(dá)到每個(gè)閥值的時(shí)候,對(duì)象從新生代晉升至老年代。如果該參數(shù)配置為1,那么從新聲代的Eden區(qū)直接移至老年代。默認(rèn)值是15,可以再Survivor區(qū)交換14次之后,晉升至老年代。晉升流程圖如下:


對(duì)象分配與GC流程圖

? ? ? ? 如圖所示,如果Survivor區(qū)無法放下,或者超大對(duì)象的閾值超過上限,則嘗試在老年代中進(jìn)行分配;如果老年代也無法放下,則會(huì)觸發(fā)FGC。如果依然無法放下,則拋出OOM。堆內(nèi)存出現(xiàn)OOM的概率是所有內(nèi)存耗盡異常中最高的。出錯(cuò)時(shí)的堆內(nèi)信息對(duì)解決問題非常有幫助,所以給JVM設(shè)置運(yùn)行參數(shù)-XX:+HeapDumpOnOutOfMemoryError,讓JVM遇到OOM異常時(shí)能輸出堆內(nèi)信息,特別是對(duì)相隔數(shù)月才出現(xiàn)的OOM異常尤為重要。

? ? ? ? 在不同的JVM實(shí)現(xiàn)及不同的回收機(jī)制中,堆內(nèi)存的劃分方式是不一樣的。


? ? ? ? 2? Metaspace(元空間)

? ? ? ? 注:JDK8使用元空間替換了永久代,在JDK8及以上版本中,設(shè)定MaxPermSize參數(shù),JVM在啟動(dòng)時(shí)并不會(huì)報(bào)錯(cuò),但是會(huì)提示Hotspot已刪除該設(shè)置項(xiàng)。

? ? ? ? 元空間在本地內(nèi)存中分配。在JDK8中,字符串常量在堆內(nèi)存,其它內(nèi)容包括類元信息、字段、靜態(tài)屬性、方法、常量等都在元空間內(nèi)。


? ? ? ? 3? JVM Stack(虛擬機(jī)棧)

? ? ? ? 棧(Stack)是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),就像子彈的彈夾,最后壓入的子彈先發(fā)射,壓在底部的子彈最后發(fā)射,撞針只能訪問位于頂部的那一顆子彈。

? ? ? ? 相當(dāng)于基于寄存器的運(yùn)行環(huán)境來說,JVM是基于棧結(jié)構(gòu)的運(yùn)行環(huán)境,棧結(jié)構(gòu)移植性更好,可控性更強(qiáng)。JVM中的虛擬機(jī)棧是描述Java方法執(zhí)行的內(nèi)存區(qū)域,它是線程私有的。棧中的元素用于支持虛擬機(jī)進(jìn)行方法調(diào)用,每個(gè)方法從開始調(diào)用到執(zhí)行完成的過程,就是棧幀從入棧到出棧的過程。在活動(dòng)線程中,只有位于棧頂?shù)膸攀怯行У?,成為?dāng)前棧幀。賑災(zāi)執(zhí)行的方法稱為當(dāng)前方法,棧幀是方法運(yùn)行的基本結(jié)構(gòu)。在執(zhí)行引擎運(yùn)行時(shí),所有指令都只能針對(duì)當(dāng)前棧幀進(jìn)行操作。而StackOverflowError表示請(qǐng)求的棧溢出,導(dǎo)致內(nèi)存耗盡,通常出現(xiàn)在遞歸方法中。操作棧的壓棧與出棧如圖所示:


操作棧的壓棧與出棧

? ? ? ? 虛擬機(jī)棧通過壓棧和出棧的方式,對(duì)每個(gè)方法對(duì)應(yīng)的活動(dòng)棧幀進(jìn)行運(yùn)算處理,方法正常執(zhí)行結(jié)束,肯定會(huì)跳轉(zhuǎn)到另一個(gè)棧幀上。在執(zhí)行的過程中,如果出現(xiàn)異常,會(huì)進(jìn)行異?;厮?,返回地址通過異常處理表確定。棧幀在整個(gè)JVM體系中的地位頗高,包括局部變量表、操作棧、動(dòng)態(tài)連接、方法返回地址等。

? ? ? ? (1)? 局部變量表

? ? ? ? 局部變量表是存放方法參數(shù)和局部變量的區(qū)域。相對(duì)于類屬性變量的準(zhǔn)備階段和初始化階段來說,局部變量沒有準(zhǔn)備階段,必須顯示初始化。如果是非靜態(tài)方法,則在index[0]的位置上存儲(chǔ)的是方法所屬對(duì)象的實(shí)例引用,隨后存儲(chǔ)的是參數(shù)和局部變量。字節(jié)碼指令中的STORE指令就是將操作棧中計(jì)算完成的局部變量寫回局部變量表的存儲(chǔ)空間內(nèi)。

? ? ? ? (2)? 操作棧

? ? ? ? 操作棧是一個(gè)初始狀態(tài)為空的桶式結(jié)構(gòu)棧。在方法執(zhí)行過程中,會(huì)有各種指令往棧中寫入和提取信息。JVM的執(zhí)行引擎是基于棧的執(zhí)行引擎,其中的棧指的就是操作棧。

? ? ? ? (3)? 動(dòng)態(tài)連接

? ? ? ? 每個(gè)棧幀中包含一個(gè)在常量池中對(duì)當(dāng)前方法的引用,目的是支持方法調(diào)用過程的動(dòng)態(tài)連接。

? ? ? ? (4)? 方法返回地址

? ? ? ? 方法執(zhí)行時(shí)有兩種退出情況:

? ? ? ? 第一,正常退出,即正常執(zhí)行到任何方法的返回字節(jié)碼指令,如RETURN、IRETURN、ARETURN等;

? ? ? ? 第二,異常退出。

? ? ? ? 無論何種退出情況,都將返回至方法當(dāng)前被調(diào)用的位置。方法退出的過程相當(dāng)于彈出當(dāng)前棧幀,退出可能有三種方式:

????????? 返回值壓入上層調(diào)用棧幀。

? ? ? ? ? 異常信息拋給能夠處理的棧幀。

? ? ? ? ? PC計(jì)數(shù)器指向方法調(diào)用后的下一條指令。


? ? ? ? 4? Native Method Stacks(本地方法棧)

? ? ? ? 本地方法棧在JVM內(nèi)存布局中,也是現(xiàn)成對(duì)象私有的,但是虛擬機(jī)棧"主內(nèi)",而本地方法棧"主外"。這個(gè)"內(nèi)外"是針對(duì)JVM來說的,本地方法棧為Native方法服務(wù)?,F(xiàn)成開始調(diào)用本地方法時(shí),會(huì)進(jìn)入一個(gè)不再受JVM約束的世界。本地方法可以通過JNI(Java Native Interface)來訪問虛擬機(jī)運(yùn)行時(shí)的數(shù)據(jù)區(qū),甚至可以調(diào)用寄存器,具有和JVM相同的能力和權(quán)限。當(dāng)大量本地方法出現(xiàn)時(shí),勢(shì)必會(huì)削弱JVM對(duì)系統(tǒng)的控制力,因?yàn)樗某鲥e(cuò)信息都比較黑盒。對(duì)于內(nèi)存不足的情況,本地方法棧還是會(huì)拋出native heap OutOfMemory。

? ? ? ? 順便說一下JNI,如果在項(xiàng)目過程中大量使用其它語言來實(shí)現(xiàn)JNI,相當(dāng)于喪失了Java的擴(kuò)平臺(tái)特性,而且增加了額外的不可控因素,威脅到程序運(yùn)行的穩(wěn)定性。假如真需要與本地代碼交互,可以用中間件、服務(wù)接口的方式進(jìn)行解耦,這樣即使本地方法崩潰也不至于影響到JVM的穩(wěn)定。


? ? ? ? 5? Program Counter Register(程序計(jì)數(shù)寄存器)

? ? ? ? 在程序計(jì)數(shù)寄存器中,Register的命名源于CPU的寄存器,CPU只有把數(shù)據(jù)裝載到寄存器才能夠運(yùn)行。寄存器存儲(chǔ)指令相關(guān)的現(xiàn)場(chǎng)信息,由于CPU時(shí)間片輪限制,眾多線程在并發(fā)執(zhí)行過程中,任何一個(gè)確定的時(shí)刻,一個(gè)處理器或者多核處理器中的一個(gè)內(nèi)核,只會(huì)執(zhí)行某個(gè)線程中的一條指令。這樣必然導(dǎo)致經(jīng)常中斷或恢復(fù),如何保證分毫無差呢?每個(gè)線程在創(chuàng)建后,都會(huì)產(chǎn)生自己的程序計(jì)數(shù)器和棧幀,程序計(jì)數(shù)器用來存放執(zhí)行指令的偏移量和行號(hào)指示器等,線程執(zhí)行或恢復(fù)都需要依賴程序計(jì)數(shù)器。程序計(jì)數(shù)器在各個(gè)線程之間互不影響,此區(qū)域也不會(huì)發(fā)生內(nèi)存溢出異常。

? ? ? ? 最后,從線程共享的角度來看,堆和元空間是所有線程共享的,而虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器是線程內(nèi)部私有的,從這個(gè)角度看一下Java內(nèi)存結(jié)構(gòu),如下圖所示。


Java的線程與內(nèi)存
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容