JVM之運(yùn)行時(shí)數(shù)據(jù)區(qū)

簡(jiǎn)介

Java虛擬機(jī)(即JVM)在Java程序運(yùn)行的過(guò)程中,會(huì)將它所管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,這些區(qū)域有的隨著JVM的啟動(dòng)而創(chuàng)建,有的隨著用戶線程的啟動(dòng)和結(jié)束而建立和銷毀。
除此之外,JVM的內(nèi)存管理機(jī)制使得不需要再為每一個(gè)新的操作去刪除/免費(fèi)代碼,由機(jī)器代替程序員這樣就不容易出現(xiàn)內(nèi)存泄露和內(nèi)存溢出的問(wèn)題了,但是一旦出現(xiàn)了這種問(wèn)題如果不了解JVM是怎樣使用內(nèi)存的,那么排查錯(cuò)誤將會(huì)非常困難。一個(gè)基本的JVM運(yùn)行時(shí)內(nèi)存模型如下所示:

JDK1.7 JVM內(nèi)存模型

程序運(yùn)行時(shí)可能只有一個(gè)線程,也可能有多個(gè)線程共同執(zhí)行,而方法區(qū)和堆是程序的所有線程所共享的內(nèi)存區(qū)域,而程序寄存器、虛擬機(jī)棧和本地方法棧則是每個(gè)線程獨(dú)占的內(nèi)存區(qū)域。

一、程序計(jì)數(shù)器(Program Counter Register)

1.什么是程序計(jì)數(shù)器

程序計(jì)數(shù)器是一個(gè)記錄著當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器
  JAVA代碼編譯后的字節(jié)碼在未經(jīng)過(guò)JIT(實(shí)時(shí)編譯器)編譯前,其執(zhí)行方式是通過(guò)“字節(jié)碼解釋器”進(jìn)行解釋執(zhí)行。簡(jiǎn)單的工作原理為解釋器讀取裝載入內(nèi)存的字節(jié)碼,按照順序讀取字節(jié)碼指令。讀取一個(gè)指令后,將該指令“翻譯”成固定的操作,并根據(jù)這些操作進(jìn)行分支、循環(huán)、跳轉(zhuǎn)等流程。
  從上面的描述中,可能會(huì)產(chǎn)生程序計(jì)數(shù)器是否是多余的疑問(wèn)。因?yàn)檠刂噶畹捻樞驁?zhí)行下去,即使是分支跳轉(zhuǎn)這樣的流程,跳轉(zhuǎn)到指定的指令處按順序繼續(xù)執(zhí)行是完全能夠保證程序的執(zhí)行順序的。假設(shè)程序永遠(yuǎn)只有一個(gè)線程,這個(gè)疑問(wèn)沒(méi)有任何問(wèn)題,也就是說(shuō)并不需要程序計(jì)數(shù)器。但實(shí)際上程序是通過(guò)多個(gè)線程協(xié)同合作執(zhí)行的。
  首先我們要搞清楚JVM的多線程實(shí)現(xiàn)方式。JVM的多線程是通過(guò)CPU時(shí)間片輪轉(zhuǎn)(即線程輪流切換并分配處理器執(zhí)行時(shí)間)算法來(lái)實(shí)現(xiàn)的。也就是說(shuō),某個(gè)線程在執(zhí)行過(guò)程中可能會(huì)因?yàn)闀r(shí)間片耗盡而被掛起,而另一個(gè)線程獲取到時(shí)間片開(kāi)始執(zhí)行。當(dāng)被掛起的線程重新獲取到時(shí)間片的時(shí)候,它要想從被掛起的地方繼續(xù)執(zhí)行,就必須知道它上次執(zhí)行到哪個(gè)位置,在JVM中,通過(guò)程序計(jì)數(shù)器來(lái)記錄某個(gè)線程的字節(jié)碼執(zhí)行位置。因此,程序計(jì)數(shù)器是具備線程隔離的特性,也就是說(shuō),每個(gè)線程工作時(shí)都有屬于自己的獨(dú)立計(jì)數(shù)器。

2.程序計(jì)數(shù)器的特點(diǎn)
  • 線程隔離性,每個(gè)線程工作時(shí)都有屬于自己的獨(dú)立計(jì)數(shù)器,即程序計(jì)數(shù)器是線程私有的
  • 執(zhí)行Java方法時(shí),程序計(jì)數(shù)器是有值的,且記錄的是正在執(zhí)行的字節(jié)碼指令的地址
  • 執(zhí)行native本地方法時(shí),程序計(jì)數(shù)器的值為空(Undefined)。因?yàn)閚ative方法是java通過(guò)JNI直接調(diào)用本地C/C++庫(kù),可以近似的認(rèn)為native方法相當(dāng)于C/C++暴露給java的一個(gè)接口,java通過(guò)調(diào)用這個(gè)接口從而調(diào)用到C/C++方法。由于該方法是通過(guò)C/C++而不是java進(jìn)行實(shí)現(xiàn)。那么自然無(wú)法產(chǎn)生相應(yīng)的字節(jié)碼,并且C/C++執(zhí)行時(shí)的內(nèi)存分配是由自己語(yǔ)言決定的,而不是由JVM決定的。
  • 程序計(jì)數(shù)器占用內(nèi)存很小,在進(jìn)行JVM內(nèi)存計(jì)算時(shí),可以忽略不計(jì)。
  • 程序計(jì)數(shù)器,是唯一一個(gè)在java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError的區(qū)域。

二、Java虛擬機(jī)棧(VM Stack)

1、什么是Java虛擬機(jī)棧
  • 用于作用于方法執(zhí)行的一塊Java內(nèi)存區(qū)域
  • 虛擬機(jī)棧是用于描述java方法執(zhí)行的內(nèi)存模型。
  • 每個(gè)java方法在執(zhí)行時(shí),會(huì)創(chuàng)建一個(gè)“棧幀(stack frame)”,棧幀的結(jié)構(gòu)分為“局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口”幾個(gè)部分(具體的作用會(huì)在字節(jié)碼執(zhí)行引擎章節(jié)中講到,這里只需要了解棧幀是一個(gè)方法執(zhí)行時(shí)所需要數(shù)據(jù)的結(jié)構(gòu))
2、特點(diǎn)
  • Java虛擬機(jī)棧也是線程私有的,它的生命周期與線程相同(隨線程而生,隨線程而滅)

  • 如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowError異常

  • 如果虛擬機(jī)??梢詣?dòng)態(tài)擴(kuò)展,如果擴(kuò)展時(shí)無(wú)法申請(qǐng)到足夠的內(nèi)存,就會(huì)拋出OutOfMemoryError異常

  • Java虛擬機(jī)棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個(gè)方法執(zhí)行的同時(shí)會(huì)創(chuàng)建一個(gè)棧幀

  • 對(duì)于我們來(lái)說(shuō),主要關(guān)注的stack棧內(nèi)存,就是虛擬機(jī)棧中局部變量表部分。

3、棧幀
虛擬機(jī)棧結(jié)構(gòu)圖
  • 棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的java虛擬機(jī)棧的棧元素。
  • 棧幀存儲(chǔ)了方法的局部變量表、操作數(shù)棧、動(dòng)態(tài)連接和方法返回地址等信息
  • 每一個(gè)方法從調(diào)用開(kāi)始至執(zhí)行完成的過(guò)程,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)里面從入棧到出棧的過(guò)程
    注意
    在編譯程序代碼的時(shí)候,棧幀中需要多大的局部變量表內(nèi)存,多深的操作數(shù)棧都已經(jīng)完全確定了。
    因此一個(gè)棧幀需要分配多少內(nèi)存,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)。
    #######棧幀結(jié)構(gòu)如下:
4、局部變量表
1.局部變量表(Local Variable Table)是一組變量值存儲(chǔ)空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。并且在Java編譯為Class文件時(shí),就已經(jīng)確定了該方法所需要分配的局部變量表的最大容量。
2.局部變量表存放了編譯期可知的各種基本數(shù)據(jù)類型(boolean、byte、char、short、int、float、long、double)「String是引用類型」,對(duì)象引用(reference類型) 和 returnAddress類型(它指向了一條字節(jié)碼指令的地址)

注意
很多人說(shuō):基本數(shù)據(jù)和對(duì)象引用存儲(chǔ)在棧中。
當(dāng)然這種說(shuō)法雖然是正確的,但是很不嚴(yán)謹(jǐn),只能說(shuō)這種說(shuō)法針對(duì)的是局部變量。
局部變量存儲(chǔ)在局部變量表中,隨著線程而生,線程而滅。并且線程間數(shù)據(jù)不共享。但是,如果是成員變量,或者定義在方法外對(duì)象的引用,它們存儲(chǔ)在堆中。
因?yàn)樵诙阎校蔷€程共享數(shù)據(jù)的,并且棧幀里的命名就已經(jīng)清楚的劃分了界限 : 局部變量表!

5、reference(對(duì)象實(shí)例的引用)

個(gè)人感覺(jué)和指針類似
一般來(lái)說(shuō),虛擬機(jī)都能從引用中直接或者間接的查找到對(duì)象的以下兩點(diǎn) :
a.在Java堆中的數(shù)據(jù)存放的起始地址索引。
b.所屬數(shù)據(jù)類型在方法區(qū)中的存儲(chǔ)類型。
例如:我們?cè)趧?chuàng)建一個(gè)Student對(duì)象時(shí)的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu):

6、案例

來(lái)段代碼試求程序運(yùn)行時(shí)虛擬機(jī)棧的內(nèi)存長(zhǎng)度,拋出StackOverflowError異常

package yzl.swu.practice;

 /** 測(cè)試代碼設(shè)計(jì)思路
 * 修改默認(rèn)堆棧大小后,利用遞歸調(diào)用一個(gè)方法,達(dá)到棧深度過(guò)大的異常目的,同時(shí)在遞歸調(diào)用過(guò)程中記錄調(diào)用此次,得出最大深度的數(shù)據(jù)
 * jvm參數(shù)
 * -Xss 180k:設(shè)置每個(gè)線程的堆棧大小(最小180k),默認(rèn)是1M
 */
public class TestStackOverflowErrorDemo {
    //棧深度統(tǒng)計(jì)值
    private int stackLength = 1;

    /**
     * 遞歸方法,導(dǎo)致棧深度過(guò)大異常
     */
    public void stackLeak() {
        stackLength++;
        stackLeak();
    }

    /**
     * 啟動(dòng)方法
     * 測(cè)試結(jié)果:當(dāng)-Xss 180k為180k時(shí),stackLength~=1544,隨著-Xss參數(shù)變大時(shí)stackLength值隨之變大
     * @param args
     */
    public static void main(String[] args) {
        TestStackOverflowErrorDemo demo = new TestStackOverflowErrorDemo();
        try {
            demo.stackLeak();
        } catch (Throwable e) {
            System.out.println("當(dāng)前棧深度:stackLength=" + demo.stackLength);
            e.printStackTrace();
        }
    }
}

三、本地方法棧(Native Method Stack)

  • 用于作用域本地方法執(zhí)行的一塊Java內(nèi)存區(qū)域
  • 本地方法棧的功能和特點(diǎn)類似于虛擬機(jī)棧,均具有線程隔離的特點(diǎn)以及都能拋出StackOverflowError和OutOfMemoryError異常。
  • 不同的是,本地方法棧服務(wù)的對(duì)象是JVM執(zhí)行的native方法(java代碼中使用native關(guān)鍵字標(biāo)記的方法),而虛擬機(jī)棧服務(wù)的是JVM執(zhí)行的java方法。如何去服務(wù)native方法?native方法使用什么語(yǔ)言實(shí)現(xiàn)?怎么組織像棧幀這種為了服務(wù)方法的數(shù)據(jù)結(jié)構(gòu)?虛擬機(jī)規(guī)范并未給出強(qiáng)制規(guī)定,因此不同的虛擬機(jī)實(shí)可以進(jìn)行自由實(shí)現(xiàn),我們常用的HotSpot虛擬機(jī)選擇合并了虛擬機(jī)棧和本地方法棧。

四、堆(Heap)

1、什么是Java堆

對(duì)于大多數(shù)應(yīng)用來(lái)說(shuō),堆是JVM所管理的內(nèi)存中最大的一塊,也是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存,可謂是對(duì)象的大本營(yíng)。此外,堆也是垃圾收集器(GC)管理的主要區(qū)域

2、堆的特點(diǎn)
  • 一個(gè)jvm實(shí)例只存在一個(gè)堆內(nèi)存,堆也是java內(nèi)存管理的核心區(qū)域
  • Java堆區(qū)在JVM啟動(dòng)的時(shí)候即被創(chuàng)建,其空間大小也就確定了。是JVM管理的最大一塊內(nèi)存空間(堆內(nèi)存的大小是可以調(diào)節(jié)的)
  • 《Java虛擬機(jī)規(guī)范》規(guī)定,堆可以處于物理上不連續(xù)的內(nèi)存空間中,但在邏輯上它應(yīng)該被視為連續(xù)的
  • 《Java虛擬機(jī)規(guī)范》中對(duì)java堆的描述是:所有的對(duì)象實(shí)例以及數(shù)組都應(yīng)當(dāng)在運(yùn)行時(shí)分配在堆上。從實(shí)際使用的角度看,“幾乎”所有的對(duì)象的實(shí)例都在這里分配內(nèi)存
  • 數(shù)組或?qū)ο笥肋h(yuǎn)不會(huì)存儲(chǔ)在棧上,因?yàn)闂斜4嬉?,這個(gè)引用指向?qū)ο蠡蛘邤?shù)組在堆中的位置(String也是引用對(duì)象哦)
  • 在方法結(jié)束后,堆中的對(duì)象不會(huì)馬上被移除,僅僅在垃圾收集的時(shí)候才會(huì)被移除
  • 堆在邏輯上劃分為“新生代”和“老年代”。由于JAVA中的對(duì)象大部分是朝生夕滅,還有一小部分能夠長(zhǎng)期的駐留在內(nèi)存中,為了對(duì)這兩種對(duì)象進(jìn)行最有效的回收,將堆劃分為新生代和老年代,并且執(zhí)行不同的回收策略。不同的垃圾收集器對(duì)這2個(gè)邏輯區(qū)域的回收機(jī)制不盡相同。
3、堆的OutOfMemoryError異常
  • 當(dāng)堆無(wú)法分配對(duì)象內(nèi)存且無(wú)法再擴(kuò)展時(shí),會(huì)拋出OutOfMemoryError異常。
  • 一般來(lái)說(shuō),堆無(wú)法分配對(duì)象時(shí)會(huì)進(jìn)行一次GC,如果GC后仍然無(wú)法分配對(duì)象,才會(huì)報(bào)內(nèi)存耗盡的錯(cuò)誤。
    代碼測(cè)試一下:
public class Test {
    public static void main(String[] args) {
        List list = new ArrayList();
        while (true) {
            //重復(fù)的向list內(nèi)添加1MB大小的數(shù)據(jù),由于list內(nèi)元素不符合GC回收條件進(jìn)而導(dǎo)致OOM。
            list.add(new byte[1024 * 1024]);
        }
    } 
}

五、Java方法區(qū)(Method Area)

1、什么是Java方法區(qū)

方法區(qū),也稱非堆(Non-Heap,與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。
方法區(qū)結(jié)構(gòu)圖如下:

2、運(yùn)行時(shí)常量池

首先需要知道常量池和運(yùn)行時(shí)常量池的區(qū)別。

  • 常量池
    即指class文件常量池,是class文件的一部分。java文件被編譯成class文件之后,除了包含了類的版本、字段、方法、接口等描述信息,還有一項(xiàng)信息叫做class文件常量池。其用于存放編譯期生成的各種字面量和符號(hào)引用。
  • 運(yùn)行時(shí)常量池
    Java語(yǔ)言并不要求常量一定只能在編譯期產(chǎn)生,運(yùn)行期間也可能產(chǎn)生新的常量,這些常量被放在運(yùn)行時(shí)常量池中。
    類加載后,常量池中的數(shù)據(jù)會(huì)在運(yùn)行時(shí)常量池中存放!
    這里所說(shuō)的常量包括:基本類型包裝類(包裝類不管理浮點(diǎn)型,整形只會(huì)管理-128到127)和String(也可以通過(guò)String.intern()方法可以強(qiáng)制將String放入常量池)
  • 字符串常量池
    HotSpot VM里,記錄interned string的一個(gè)全局表叫做StringTable,它本質(zhì)上就是個(gè)HashSet<String>。注意它只存儲(chǔ)對(duì)java.lang.String實(shí)例的引用,而不存儲(chǔ)String對(duì)象的內(nèi)容
注意:jdk 1.7后,移除了方法區(qū)間,運(yùn)行時(shí)常量池和字符串常量池都在堆中。
3、方法區(qū)的實(shí)現(xiàn)

具體放在哪里,不同的實(shí)現(xiàn)可以放在不同的地方。永久代是HotSpot虛擬機(jī)特有的概念,是對(duì)方法區(qū)的實(shí)現(xiàn),別的JVM沒(méi)有永久代的概念。(雖然去除了永久代,但是方法區(qū)作為概念上的區(qū)域仍然存在)
方法區(qū)的實(shí)現(xiàn),虛擬機(jī)規(guī)范中并未明確規(guī)定,目前有2種比較主流的實(shí)現(xiàn)方式:

  • HotSpot虛擬機(jī)1.7-:在JDK1.6及之前版本,HotSpot使用“永久代(permanent generation)”的概念作為實(shí)現(xiàn),即將GC分代收集擴(kuò)展至方法區(qū)。這種實(shí)現(xiàn)比較偷懶,可以不必為方法區(qū)編寫專門的內(nèi)存管理,但帶來(lái)的后果是容易碰到內(nèi)存溢出的問(wèn)題(因?yàn)橛谰么?XX:MaxPermSize的上限)。在JDK1.7+之后,HotSpot逐漸改變方法區(qū)的實(shí)現(xiàn)方式,如1.7版本移除了方法區(qū)中的字符串常量池。
  • HotSpot虛擬機(jī)1.8+:1.8版本中移除了方法區(qū)并使用metaspace(元數(shù)據(jù)空間)作為替代實(shí)現(xiàn)。metaspace占用系統(tǒng)內(nèi)存,也就是說(shuō),只要不碰觸到系統(tǒng)內(nèi)存上限,方法區(qū)會(huì)有足夠的內(nèi)存空間。但這不意味著我們不對(duì)方法區(qū)進(jìn)行限制,如果方法區(qū)無(wú)限膨脹,最終會(huì)導(dǎo)致系統(tǒng)崩潰。
    JDK1.8+ JVM
4、方法區(qū)的OutOfMemoryError
  • 首先,為什么使用“永久代”并將GC分代收集擴(kuò)展至方法區(qū)這種實(shí)現(xiàn)方式不好,會(huì)導(dǎo)致OOM?首先要明白方法區(qū)的內(nèi)存回收目標(biāo)是什么,方法區(qū)存儲(chǔ)了類的元數(shù)據(jù)信息和各種常量,它的內(nèi)存回收目標(biāo)理應(yīng)當(dāng)是對(duì)這些類型的卸載和常量的回收。但由于這些數(shù)據(jù)被類的實(shí)例引用,卸載條件變得復(fù)雜且嚴(yán)格,回收不當(dāng)會(huì)導(dǎo)致堆中的類實(shí)例失去元數(shù)據(jù)信息和常量信息。因此,回收方法區(qū)內(nèi)存不是一件簡(jiǎn)單高效的事情,往往GC在做無(wú)用功。另外隨著應(yīng)用規(guī)模的變大,各種框架的引入,尤其是使用了字節(jié)碼生成技術(shù)的框架,會(huì)導(dǎo)致方法區(qū)內(nèi)存占用越來(lái)越大,最終OOM。
  • 因?yàn)榉椒▍^(qū)最終都會(huì)有一個(gè)最大值上限,因此若方法區(qū)(含運(yùn)行時(shí)常量池)占用內(nèi)存到達(dá)其最大值,且無(wú)法再申請(qǐng)到內(nèi)存時(shí),便會(huì)拋出OutOfMemoryError。
最后編輯于
?著作權(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)容