<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          JVM 內(nèi)存布局詳解,圖文并茂,寫得太好了!

          共 5917字,需瀏覽 12分鐘

           ·

          2022-07-08 05:26


          來(lái)源:www.cnblogs.com/hyxiao97/p/15395886.html

          大家我是程序汪,今年行情差,面試也卷,面試官喜歡上來(lái)就問(wèn)JVM,來(lái)來(lái)分享一篇JVM干貨

          • 內(nèi)存布局
          • Heap 堆區(qū)
          • Metaspace 元空間
          • JVM Stacks 虛擬機(jī)棧
          • Native Method Stacks(本地方法棧)
          • Program Counter Register (程序計(jì)數(shù)寄存器)
          • 小結(jié)

          內(nèi)存布局

          JVM內(nèi)存布局規(guī)定了Java在運(yùn)行過(guò)程中內(nèi)存申請(qǐng)、分配、管理的策略,保證了JVM的穩(wěn)定高效運(yùn)行。不同的JVM對(duì)于內(nèi)存的劃分方式和管理機(jī)制存在部分差異。結(jié)合JVM虛擬機(jī)規(guī)范,一起來(lái)探討jVM的內(nèi)存布局。如下圖所示:

          圖片

          Heap 堆區(qū)

          Heap堆區(qū)是Java發(fā)生OOM(Out Of Memory)故障的地方,堆中存儲(chǔ)著我們平時(shí)創(chuàng)建的實(shí)例對(duì)象,最終這些不再使用的對(duì)象會(huì)被垃圾收集器回收掉,而且堆是線程共享的。一般情況下,堆所占用的內(nèi)存空間是JVM內(nèi)存區(qū)域中最大的,我們?cè)谄綍r(shí)編碼中,創(chuàng)建對(duì)象如果不加以克制,內(nèi)存空間也會(huì)被耗盡。

          堆的內(nèi)存空間是可以自定義大小的,同時(shí)也支持在運(yùn)行時(shí)動(dòng)態(tài)修改,通過(guò) -Xms-Xmx 這兩參數(shù)去改變堆的初始值最大值-X指的是JVM運(yùn)行參數(shù),ms 是memory start的簡(jiǎn)稱,代表的是最小堆容量mx是memory max的簡(jiǎn)稱,代表的是最大堆容量;如 -Xms256M代表堆的初始值是256M,-Xmx1024M代表堆的最大值是1024M。

          由于堆的內(nèi)存空間是可以動(dòng)態(tài)調(diào)整的,所以在服務(wù)器運(yùn)行的時(shí)候,請(qǐng)求流量的不確定性可能會(huì)導(dǎo)致我們堆的內(nèi)存空間不斷調(diào)整,會(huì)增加服務(wù)器的壓力,所以我們一般都會(huì)將JVM的XmsXmx的值設(shè)置成一樣,同樣也為了避免在GC(垃圾回收)之后調(diào)整堆大小時(shí)帶來(lái)的額外壓力。

          堆區(qū)分為兩大區(qū):Young區(qū)和Old區(qū),又稱新生代老年代。對(duì)象剛創(chuàng)建的時(shí)候,會(huì)被創(chuàng)建在新生代到一定階段之后會(huì)移送至老年代 ,如果創(chuàng)建了一個(gè)新生代無(wú)法容納的新對(duì)象,那么這個(gè)新對(duì)象也可以創(chuàng)建到老年代。如上圖所示。

          新生代分為1個(gè)Eden區(qū)和2個(gè)S區(qū),S代表Survivor。大部分的對(duì)象會(huì)在Eden區(qū)中生成,當(dāng)Eden區(qū)沒(méi)有足夠的空間容納新對(duì)象時(shí),會(huì)觸發(fā)Young Garbage Collection,即YGC。在Eden區(qū)進(jìn)行垃圾清除時(shí),它的策略是會(huì)把沒(méi)有引用的對(duì)象直接給回收掉,還有引用的對(duì)象會(huì)被移送到Survivor區(qū)

          Survivor區(qū)有S0S1兩個(gè)內(nèi)存空間,每次進(jìn)行YGC的時(shí)候,會(huì)將存活的對(duì)象復(fù)制到未使用的那塊內(nèi)存空間,然后將當(dāng)前正在使用的空間完全清除掉,再交換兩個(gè)空間的使用狀況。如果YGC要移送的對(duì)象Survivor區(qū)無(wú)法容納,那么就會(huì)將該對(duì)象直接移交給老年代。

          上面說(shuō)了,到一定階段的對(duì)象會(huì)移送到老年區(qū),這是什么意思呢?每一個(gè)對(duì)象都有一個(gè)計(jì)數(shù)器,當(dāng)每次進(jìn)行YGC的時(shí)候,都會(huì) +1。通過(guò)-XX:MAXTenuringThrehold參數(shù)可以配置當(dāng)計(jì)數(shù)器的值到達(dá)某個(gè)閾值時(shí),對(duì)象就會(huì)從新生代移送至老年代。

          該參數(shù)的默認(rèn)值為15,也就是說(shuō)對(duì)象在Survivor區(qū)中的S0和S1內(nèi)存空間交換的次數(shù)累加到15次之后,就會(huì)移送至老年代。如果參數(shù)配置為1,那么創(chuàng)建的對(duì)象就會(huì)直接移送至老年代。具體的對(duì)象分配即回收流程可觀看下圖所示。

          圖片

          如果Survivor區(qū)無(wú)法放下,或者創(chuàng)建了一個(gè)超大新對(duì)象,EdenOld區(qū)都無(wú)法存放,就會(huì)觸發(fā)Full Garbage Collection,即FGG,便再嘗試放在Old區(qū),如果還是容納不了,就會(huì)拋出OOM異常。在不同的JVM實(shí)現(xiàn)及不同的回收機(jī)制中,堆內(nèi)存的劃分方式是不一樣的。

          Metaspace 元空間

          在JDK8版本中,元空間的前身Pern區(qū)已經(jīng)被淘汰。在JDK7及之前的版本中,Hotspot還有Pern區(qū),翻譯為永久代,在啟動(dòng)時(shí)就已經(jīng)確定了大小,難以進(jìn)行調(diào)優(yōu),并且只有FGC時(shí)會(huì)移動(dòng)類元信息。不同于之前版本的Pern(永久代),JDK8的元空間已經(jīng)在本地內(nèi)存中進(jìn)行分配,并且,Pern區(qū)中的所有內(nèi)容中字符串常量移至堆內(nèi)存,其他內(nèi)容也包括了類元信息字段靜態(tài)屬性方法常量等等都移至元空間內(nèi)。

          JVM Stacks 虛擬機(jī)棧

          棧(Stack)是一個(gè)先進(jìn)后出的數(shù)據(jù)結(jié)構(gòu),先進(jìn)后出怎么理解?類似于我們平時(shí)打羽毛球時(shí),裝羽毛球的球筒,第一個(gè)先放進(jìn)去的往往最后一個(gè)才能拿出來(lái),最后放進(jìn)去的一個(gè)最先拿出來(lái)。

          相對(duì)于基于寄存器的運(yùn)行環(huán)境來(lái)說(shuō),JVM是基于棧結(jié)構(gòu)的運(yùn)行環(huán)境。因?yàn)闂=Y(jié)構(gòu)移植性更好,可控性更強(qiáng)。JVM的虛擬機(jī)棧是描述Java方法執(zhí)行的內(nèi)存區(qū)域,并且是線程私有的。棧中的元素用于支持虛擬機(jī)進(jìn)行方法調(diào)用,每個(gè)方法從開(kāi)始調(diào)用到執(zhí)行完成的過(guò)程,就是棧幀從入幀到出幀的過(guò)程。

          在活動(dòng)線程中,只有位于棧頂?shù)膸攀怯行У模Q為當(dāng)前棧幀。正在執(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)在遞歸方法中。如果把JVM當(dāng)做一個(gè)棋盤,虛擬機(jī)棧就是棋盤上的將/帥,當(dāng)前方法的棧幀就是棋子能走的區(qū)域,而操作棧就是每一個(gè)棋子。操作棧的壓棧和出棧如下圖所示:

          圖片

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

          下面對(duì)棧幀的各個(gè)活動(dòng)棧幀進(jìn)行簡(jiǎn)要的分析

          (1)局部變量表

          局部變量表是存放方法參數(shù)局部變量的區(qū)域。我們都知道,類屬性變量一共要經(jīng)歷兩個(gè)階段,分為準(zhǔn)備階段初始化階段,而局部變量是沒(méi)有準(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í)行過(guò)程中,會(huì)有各種指令往棧中寫入和提取信息。JVM的執(zhí)行引擎是基于棧的執(zhí)行引擎,其中的棧指的就是操作棧。字節(jié)碼指令集的定義都是基于棧類型的,棧的深度在方法元信息的stack屬性中,下面就通過(guò)一個(gè)例子來(lái)說(shuō)明下操作棧與局部變量表的交互:

          public int add() {
              int x = 10;
              int y = 20;
              int z = x + y;

              return z;
          }

          字節(jié)碼操作順序如下:

          public int add();
            Code:
               0: bipush        10 // 常量 10 壓入操作棧
               2: istore_1     // 并保存到局部變量表的 slot_1 中  (第 1 處)
               3: bipush        20 // 常量 20 壓入操作棧
               5: istore_2     // 并保存到局部變量表的 slot_2 中
               6: iload_1      // 把局部變量表的 slot_1 元素(int x)壓入操作棧
               7: iload_2      // 把局部變量表的 slot_2 元素(int y)壓入操作棧
               8: iadd      // 把上方的兩個(gè)數(shù)都取出來(lái),在 CPU 里加一下,并壓回操作棧的棧頂
               9: istore_3     // 把棧頂?shù)慕Y(jié)果存儲(chǔ)到局部變量表的 slot_3 中
              10: iload_3
              11: ireturn      // 返回棧頂元素值

          第 1 處說(shuō)明:局部變量表就像一個(gè)快遞柜,有著很多的柜子,依次編號(hào)為1,2,3,...,n,字節(jié)碼指令 istore_1 就代表打開(kāi)了 1 號(hào)柜子,再把棧頂中的值 10 存進(jìn)去。棧就好如一個(gè)桶,任何時(shí)候只能對(duì)桶口的元素進(jìn)行操作,所以數(shù)據(jù)只能在棧頂進(jìn)行存取。部分指令可以直接在柜子里面直接進(jìn)行,比如 iinc指令,直接對(duì)抽屜里的數(shù)值進(jìn)行 +1操作。我們經(jīng)常遇到的 i++ 和 ++i,通過(guò)字節(jié)碼對(duì)比起來(lái),答案一下子就一目了然了。如下表格所示:

          圖片

          左列中,iload_1 從局部變量表的第1號(hào)柜子取出一個(gè)數(shù),壓入棧頂,下一步直接在柜子里實(shí)現(xiàn) + 1的操作,而這個(gè)操作時(shí)對(duì)棧頂元素的值沒(méi)有任何影響,所以 istore_2 只是把棧頂元素賦值給 a,而右列,它是先在柜子里面進(jìn)行 +1的操作,然后再通過(guò) iload_1 把第1號(hào)柜子里的數(shù)壓入棧頂,所以istore_2賦給a的值是 +1 之后的值。擴(kuò)展下,i++ 并非是原子操作。即使通過(guò)volatile關(guān)鍵字來(lái)修飾,多線程情況下,還是會(huì)出現(xiàn)數(shù)據(jù)互相覆蓋的情況。

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

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

          (4)方法返回地址

          方法執(zhí)行時(shí)有兩種退出情況:第一,正常退出,即正常執(zhí)行到任何方法的返回字節(jié)碼指令,如 RETURNIRETURNARETURN等;第二,異常退出。無(wú)論何種退出情況,都將返回方法當(dāng)前被調(diào)用的位置。方法退出的過(guò)程相當(dāng)于彈出當(dāng)前棧幀,而退出可能有三種方式:

          • 返回值壓入上層調(diào)用棧幀。
          • 異常信息拋給能夠處理的棧幀。
          • PC 計(jì)數(shù)器指向方法調(diào)用后的下一條指令。

          Native Method Stacks(本地方法棧)

          本地方法棧(Native Method Stack)在JVM內(nèi)存布局中,也是線程對(duì)象私有的,但是虛擬機(jī)棧“主內(nèi)”,而本地方法棧“主外”。這個(gè)“內(nèi)外”是針對(duì)JVM來(lái)說(shuō)的,本地方法棧為Native方法服務(wù)。線程開(kāi)始調(diào)用本地方法時(shí),會(huì)進(jìn)入一個(gè)不再受JVM約束的世界。本地方法可以通過(guò)JVNI(Java Native Interface)來(lái)訪問(wèn)虛擬機(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

          重點(diǎn)說(shuō)下JNI類本地方法,最常用的本地方法應(yīng)該是System.currentTimeMills()JNI使Java深度使用操作系統(tǒng)的特性功能,復(fù)用非Java代碼。但是在項(xiàng)目過(guò)程中,如果大量使用其他語(yǔ)言來(lái)實(shí)現(xiàn)JNI,就會(huì)喪失跨平臺(tái)特性,威脅到程序運(yùn)行的穩(wěn)定性。假如需要與本地代碼交互,就可以用中間標(biāo)準(zhǔn)框架來(lái)進(jìn)行解耦,這樣即使本地方法崩潰也不至于影響到JVM的穩(wěn)定。

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

          在程序計(jì)數(shù)寄存器(Program Counter Register,PC)中,Register的命名源于CPU的寄存器,CPU只有把數(shù)據(jù)裝載到寄存器才能夠運(yùn)行。寄存器存儲(chǔ)指令相關(guān)的現(xiàn)場(chǎng)信息,由于CPU時(shí)間片輪限制,眾多線程在并發(fā)執(zhí)行過(guò)程中,任何一個(gè)確定的時(shí)刻,一個(gè)處理器或者多核處理器中的一個(gè)內(nèi)核,只會(huì)執(zhí)行某個(gè)線程中的一個(gè)指令。

          這樣必然會(huì)導(dǎo)致經(jīng)常中斷或恢復(fù),如何才能保證分毫無(wú)差呢?每個(gè)線程在創(chuàng)建之后,都會(huì)產(chǎn)生自己的程序計(jì)數(shù)器棧幀程序計(jì)數(shù)器用來(lái)存放執(zhí)行指令的偏移量和行號(hào)指示器等,線程執(zhí)行或恢復(fù)都要依賴程序計(jì)數(shù)器程序計(jì)數(shù)器在各個(gè)線程之間互不影響,此區(qū)域也不會(huì)發(fā)生內(nèi)存溢出異常

          小結(jié)

          最后,從線程的角度來(lái)看,堆和元空間是所有線程共享的,而虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器是線程內(nèi)部私有的,我們以線程的角度再來(lái)看看Java的內(nèi)存結(jié)構(gòu)圖:

          程序汪資料鏈接

          程序汪接的7個(gè)私活都在這里,經(jīng)驗(yàn)整理

          Java項(xiàng)目分享  最新整理全集,找項(xiàng)目不累啦 07版

          堪稱神級(jí)的Spring Boot手冊(cè),從基礎(chǔ)入門到實(shí)戰(zhàn)進(jìn)階

          臥槽!字節(jié)跳動(dòng)《算法中文手冊(cè)》火了,完整版 PDF 開(kāi)放下載!

          臥槽!阿里大佬總結(jié)的《圖解Java》火了,完整版PDF開(kāi)放下載!

          字節(jié)跳動(dòng)總結(jié)的設(shè)計(jì)模式 PDF 火了,完整版開(kāi)放下載!


          歡迎添加程序汪個(gè)人微信 itwang009  進(jìn)粉絲群或圍觀朋友



          瀏覽 27
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  上海人妻第三次3p | 欧美逼网 | 日韩一区在线播放 | 骚逼免费观看 | 天天玩,夜夜操 |