走進(jìn)JVM-內(nèi)存布局

內(nèi)存是非常重要的系統(tǒng)資源爽撒,是硬盤和 CPU 的中間倉庫及橋梁鸯乃,承載著操作系統(tǒng)和應(yīng)用程序的實(shí)時(shí)運(yùn)行辙喂。JVM 內(nèi)存布局規(guī)定了 Java 在運(yùn)行過程中內(nèi)存申請拌滋、分配泣懊、管理的策略跟畅,保證了JVM 的高效穩(wěn)定運(yùn)行棚蓄。不同的JVM 對于內(nèi)存的劃分方式和管理機(jī)制存在著部分差異哟楷。結(jié)合JVM 虛擬機(jī)規(guī)范瘤载,來探討一下經(jīng)典的JVM 內(nèi)存布局,如下圖所示


JVM經(jīng)典內(nèi)存布局

1.Heap(堆區(qū))

??Heap是OOM 故障最主要的發(fā)源地卖擅,它存儲(chǔ)著幾乎所有的實(shí)例對象鸣奔,堆由垃圾收集器自動(dòng)回收墨技,堆區(qū)由各子線程共享使用。通常情況下挎狸,它占用的空間是所有內(nèi)有區(qū)域中最大的扣汪,但如果無節(jié)制地創(chuàng)建大量對象,也容易消耗完所有的空間锨匆。堆的內(nèi)存空間既可以固定大小崭别,也可以在運(yùn)行時(shí)動(dòng)態(tài)地調(diào)整,通過如下參數(shù)設(shè)定初始值和最大值恐锣,比如-Xms256M-Xmx1024M茅主,其中-X表示它是JVM運(yùn)行參數(shù),ms是memorystart的簡稱土榴,mx是memory max 的簡稱诀姚,分別代表最小堆容量和最大堆容量。但是在通常情況下玷禽,服務(wù)器在運(yùn)行過程中学搜,堆空間不斷地?cái)U(kuò)容與回縮,勢必形成不必要的系統(tǒng)壓力论衍,所以在線上生產(chǎn)環(huán)境中,JVM的Xms和Xmx設(shè)置成一樣大小聚磺,避免在GC 后調(diào)整堆大小時(shí)帶來的額外壓力坯台。
??堆分成兩大塊:新生代和老年代。對象產(chǎn)生之初在新生代瘫寝,步入暮年時(shí)進(jìn)入老年代蜒蕾,但是老年代也接納在新生代無法容納的超大對象。新生代=1個(gè) Eden 區(qū) +2個(gè)Survivor 區(qū)焕阿。絕大部分對象在 Eden 區(qū)生成咪啡,當(dāng) Eden 區(qū)裝填滿的時(shí)候,會(huì)觸發(fā) Young Garbage Collection暮屡,即YGC撤摸。垃圾回收的時(shí)候,在 Eden 區(qū)實(shí)現(xiàn)清除策略褒纲,沒有被引用的對象則直接回收准夷。依然存活的對象會(huì)被移送到 Survivor 區(qū),這個(gè)區(qū)真是名副其實(shí)的存在莺掠。Survivor 區(qū)分為 SO和 S1 兩塊內(nèi)存空間衫嵌,送到哪塊空間呢? 每次YGC的時(shí)候,它們將存活的對象復(fù)制到未使用的那塊空間彻秆,然后將當(dāng)前正在使用的空間完全清除楔绞,交換兩塊空間的使用狀態(tài)结闸。如果YGC 要移送的對象大于 Survivor 區(qū)容量的上限,則直接移交給老年代酒朵。假如一些沒有進(jìn)取心的對象以為可以一直在新生代的Survivor 區(qū)交換來交換去桦锄,那就錯(cuò)了。每個(gè)對象都有一個(gè)計(jì)數(shù)器耻讽,每次YGC都會(huì)加1察纯。-XX:MaxTenuringThreshold 參數(shù)能配置計(jì)數(shù)器的值到達(dá)某個(gè)值的時(shí)候,對象從新生代晉升至老年代针肥。如果該參數(shù)配置為 1饼记,那么從新生代的 Eden 區(qū)直接移至老年代。默認(rèn)值是 15慰枕,可以在 Survivor 區(qū)交換 14 次之后具则,晉升至老年代。與上圖 匹配的對象晉升流程圖如下圖 所示具帮。


對象分配與簡要GC流程圖

??上圖中博肋,如果 Survivor區(qū)無法放下,或者超大對象的闕值超過上限蜂厅,則嘗試在老年代中進(jìn)行分配;如果老年代也無法放下匪凡,則會(huì)觸發(fā)Full Garbage Collection,即FGC掘猿。如果依然無法放下病游,則拋出OOM。堆內(nèi)存出現(xiàn)OOM的概率是所有內(nèi)存耗異常中最高的稠通。出錯(cuò)時(shí)的堆內(nèi)信息對解決問題非常有幫助衬衬,所以給JVM設(shè)置運(yùn)行參數(shù)XX:+HeapDumpOnOutOfMemoryError,讓JVM遇到OOM異常時(shí)能輸出堆內(nèi)信息特別是對相隔數(shù)月才出現(xiàn)的OOM異常尤為重要改橘。
??在不同的JVM 實(shí)現(xiàn)及不同的回收機(jī)制中滋尉,堆內(nèi)存的劃分方式是不一樣的。

2.Metaspace(元數(shù)據(jù)區(qū))

??本文章源碼解析和示例代碼基本采用JDK11版本飞主,JVM 則為 Hotspot狮惜。早在JDK8版本中,元空間的前身Perm 區(qū)已經(jīng)被淘汰碌识。在JDK7及之前的版本中讽挟,只有Hotspo才有 Perm 區(qū),譯為永久代丸冕,它在啟動(dòng)時(shí)固定大小耽梅,很難進(jìn)行調(diào)優(yōu),并且FGC 時(shí)會(huì)移動(dòng)類元信息胖烛。在某些場景下眼姐,如果動(dòng)態(tài)加載類過多诅迷,容易產(chǎn)生 Perm 區(qū)的OOM。比如某個(gè)實(shí)際 Web 工程中众旗,因?yàn)楣δ茳c(diǎn)比較多罢杉,在運(yùn)行過程中,要不斷動(dòng)態(tài)加載很的類贡歧,經(jīng)常出現(xiàn)致命錯(cuò)誤:
'Exception in thread dubbo client x.x connector’java.lang.OutOfMemoryError: PermGenspace"
為了解決該問題滩租,需要設(shè)定運(yùn)行參數(shù)-XX:MaxPermSize=1280m,如果部署到新機(jī)器上利朵,往往會(huì)因?yàn)镴VM 參數(shù)沒有修改導(dǎo)致故障再現(xiàn)律想。不熟悉此應(yīng)用的人排查問題時(shí)往往苦不堪言,除此之外绍弟,永久代在垃圾回收過程中還存在諸多問題技即。所以,JDK8使用元空間替換永久代樟遣。在JDK8及以上版本中而叼,設(shè)定MaxPermSize參數(shù),JVM 在啟動(dòng)時(shí)并不會(huì)報(bào)錯(cuò)豹悬,但是會(huì)提示: Java HotSpot 64Bit Server VM waming:ignoring option MaxPermSize-2560m; support was removed in 8.0.
??區(qū)別于永久代葵陵,元空間在本地內(nèi)存中分配。在JDK8里瞻佛,Perm 區(qū)中的所有內(nèi)容中字符串常量移至堆內(nèi)存脱篙,其他內(nèi)容包括類元信息、字段涤久、靜態(tài)屬性、方法忍弛、常量等都移動(dòng)至元空間內(nèi)响迂,比如Object 類元信息、靜態(tài)屬性 System.out细疚、整常量10000000等蔗彤。在常量池中的String,其實(shí)際對象是被保存在堆內(nèi)存中的疯兼。

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

??棧( Stack )是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),就像子彈的彈夾,最后壓入的子彈先發(fā)射壓在底部的子彈最后發(fā)射然遏,撞針只能訪問位于頂部的那一顆子彈。
??相對于基于寄存器的運(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ù)馁嚥攀怯行У氖鄣Q為當(dāng)前棧幀斤葱。正在執(zhí)行的方法稱為當(dāng)前方法,棧幀是方法運(yùn)行的基本結(jié)構(gòu)揖闸。在執(zhí)行引擎運(yùn)行時(shí)揍堕,所有指令都只能針對當(dāng)前棧進(jìn)行操作。而 StackOverflowError表示請求的棧溢出楔壤,導(dǎo)致內(nèi)存耗盡鹤啡,通常出現(xiàn)在遞歸方法中。JVM 能夠橫掃千軍蹲嚣,慮擬機(jī)棧就是它的心腹大將递瑰,當(dāng)前方法的棧幀,都是正在戰(zhàn)斗的戰(zhàn)場隙畜,其中的操作棧是參與戰(zhàn)斗的士兵抖部。操作棧的壓棧與出棧如下圖所示。


操作棧的壓棧與出棧

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

  1. 局部變量表
    ??局部變量表是存放方法參數(shù)和局部變量的區(qū)域熊榛。相對于類屬性變量的準(zhǔn)備階段和初始化階段來說,局部變量沒有準(zhǔn)備階段腕巡,必須顯式初始化玄坦。如果是非靜態(tài)方法,在index[0]位置上存儲(chǔ)的是方法所屬對象的實(shí)例引用绘沉,隨后存儲(chǔ)的是參數(shù)和局部變量营搅。字節(jié)碼指令中的 STORE 指令就是將操作棧中計(jì)算完成的局部變量寫回局部變量表存儲(chǔ)空間內(nèi)云挟。
  2. 操作棧
    ??操作棧是一個(gè)初始狀態(tài)為空的桶式結(jié)構(gòu)棧。在方法執(zhí)行過程中转质,會(huì)有各種指令往棧中寫入和提取信息园欣。JVM 的執(zhí)行引擎是基于棧的執(zhí)行引擎,其中的棧指的就是操作棧休蟹。字節(jié)碼指令集的定義都是基于棧類型的沸枯,棧的深度在方法元信息的 stack屬性中下面用一段簡單的代碼說明操作棧與局部變量表的交互:
    public int simpleMethod() {
        int x = 13;
        int y = 14;
        int z = x + y;
        return z;
    }

詳細(xì)的字節(jié)碼操作順序如下(Intellij IDEA 中查看字節(jié)碼 View -> Show bytecode):

  // access flags 0x1
  public simpleMethod()I
   L0
    LINENUMBER 25 L0
    BIPUSH 13 // 常量13壓入操作棧
    ISTORE 1 // 并保存到局部凌量表的 slot 1中 (第1處)
   L1
    LINENUMBER 26 L1
    BIPUSH 14 //常量14壓入操作找,注意是BIPUSH
    ISTORE 2 //并保存到局部變量表的sIot 2中
   L2
    LINENUMBER 27 L2
    ILOAD 1 //把局部變量表的 slot 1尤素 (int x)壓入操作棧
    ILOAD 2 // 把局部變量表的 slot 2 元素 (int y)壓入操作棧
    IADD //把上方的兩個(gè)數(shù)都取出來赂弓,在 CPU 里加一下绑榴,并壓回操作棧的棧頂
    ISTORE 3 //把棧頂?shù)慕Y(jié)果存儲(chǔ)到局部變量表的 slot 3 中
   L3
    LINENUMBER 28 L3
    ILOAD 3
    IRETURN //返回棧頂元素值
   L4
    LOCALVARIABLE this Lcom/linkmiao/iot/demo/test/d202311/TestWhoLoad; L0 L4 0
    LOCALVARIABLE x I L1 L4 1
    LOCALVARIABLE y I L2 L4 2
    LOCALVARIABLE z I L3 L4 3
    MAXSTACK = 2  // // 最大 深度為 2,局部變量個(gè)數(shù)為4
    MAXLOCALS = 4

第1處說明:局部變量表就像一個(gè)中藥柜盈魁,里面有很多抽展翔怎,依次編號為 0,1杨耙,2.3赤套,·,n珊膜,字節(jié)碼指令I(lǐng)STORE1就是打開1號抽屜容握,把棧頂中的數(shù)13存進(jìn)去。棧是一個(gè)很深的豎桶车柠,任何時(shí)候只能對桶口元素進(jìn)行操作剔氏,所以數(shù)據(jù)只能在棧頂進(jìn)行存取。某些指令可以直接在抽屜里進(jìn)行竹祷,比如 iinc 指令谈跛,直接對抽屜里的數(shù)值進(jìn)行 +1操作。

  1. 動(dòng)態(tài)連接
    每個(gè)棧幀中包含一個(gè)在常量池中對當(dāng)前方法的引用塑陵,目的是支持方法調(diào)用過程的動(dòng)態(tài)連接感憾。
  2. 方法返回地址
    方法執(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 Slacks(本地方法棧)

??本地方法棧(Native Method Stack)在JVM 內(nèi)存布局中柬祠,也是線程對象私有的,但是虛擬機(jī)棻毕罚“主內(nèi)”,而本地方法椔祝“主外”嗜愈。這個(gè)“內(nèi)外”是針對JVM 來說的,本地方法棧為 Native方法服務(wù)。線程開始調(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í),勢必會(huì)削弱JVM 對系統(tǒng)的控制力搂赋,因?yàn)樗某鲥e(cuò)信息都比較黑盒赘阀。對于內(nèi)有不足的情況,本地方法棧還是會(huì)拋出native heap OutOfMemory厂镇。
??重點(diǎn)說一下JNI類本地方法纤壁,最著名的本地方法應(yīng)該是Systen.currentTimeMillis(),JNI使 Java 深度使用操作系統(tǒng)的特性功能捺信,復(fù)用非 Java代碼酌媒。但是在項(xiàng)自過程中,如果大量使用其他語言來實(shí)現(xiàn)JNI迄靠,就會(huì)喪失跨平臺(tái)特性秒咨,威脅到程序運(yùn)行的穩(wěn)定性。假如需要與本地代碼交互掌挚,就可以用中間標(biāo)準(zhǔn)框架進(jìn)行解耦.這樣即使本地方法崩潰也不至于影響到JVM的穩(wěn)定雨席。當(dāng)然,如果要求極高的執(zhí)行效率偏底層的跨進(jìn)程操作等吠式,可以考慮設(shè)計(jì)為JNI調(diào)用方式陡厘。

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

??在程序計(jì)數(shù)寄存器(Program Counter Register,PC)中特占,Register 的命名源于CPU的寄存器糙置,CPU只有把數(shù)據(jù)裝載到寄存器才能夠運(yùn)行。寄存器存儲(chǔ)指令相關(guān)的現(xiàn)場信息是目,由于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í)行指令的偏移量和行號指示器等,線程執(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)容合作請聯(lián)系作者
  • 序言:七十年代末扶踊,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子郎任,更是在濱河造成了極大的恐慌秧耗,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舶治,死亡現(xiàn)場離奇詭異分井,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)霉猛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進(jìn)店門尺锚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人惜浅,你說我怎么就攤上這事瘫辩。” “怎么了坛悉?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵伐厌,是天一觀的道長。 經(jīng)常有香客問我裸影,道長挣轨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任空民,我火速辦了婚禮刃唐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘界轩。我一直安慰自己画饥,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布浊猾。 她就那樣靜靜地躺著抖甘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪葫慎。 梳的紋絲不亂的頭發(fā)上衔彻,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天,我揣著相機(jī)與錄音偷办,去河邊找鬼艰额。 笑死,一個(gè)胖子當(dāng)著我的面吹牛椒涯,可吹牛的內(nèi)容都是我干的柄沮。 我是一名探鬼主播,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼废岂,長吁一口氣:“原來是場噩夢啊……” “哼祖搓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起湖苞,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤拯欧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后财骨,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體镐作,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年隆箩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了滑肉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡摘仅,死狀恐怖靶庙,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情娃属,我是刑警寧澤六荒,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站矾端,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏秩铆。R本人自食惡果不足惜砚亭,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望添祸。 院中可真熱鬧,春花似錦署尤、人聲如沸耙替。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至箕别,卻和暖如春狐援,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背究孕。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工啥酱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人厨诸。 一個(gè)月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓镶殷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親微酬。 傳聞我的和親對象是個(gè)殘疾皇子绘趋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內(nèi)容