程序運(yùn)行時(shí),內(nèi)存到底是如何進(jìn)行分配的?

Java運(yùn)行時(shí)內(nèi)存分配

將 Java 內(nèi)存分為 堆內(nèi)存(heap)棧內(nèi)存(Stack)并不準(zhǔn)確,Java 的內(nèi)存區(qū)域劃分實(shí)際上更為復(fù)雜。

Java 虛擬機(jī)在執(zhí)行 Java 程序的過程中,會(huì)把它所管理的內(nèi)存劃分為不同的數(shù)據(jù)區(qū)域:

Java運(yùn)行時(shí)內(nèi)存分配

上圖中:

  1. HelloWorld.java 會(huì)經(jīng)過編輯生成 HelloWorld.class 字節(jié)碼文件。
  2. Java 虛擬中要想訪問 HelloWorld 這個(gè)類時(shí),需要通過 類加載器(ClassLoader) 進(jìn)行加載,將 HelloWorld.class 字節(jié)碼文件加載到 JVM 內(nèi)存中。
  3. JVM 內(nèi)存可劃分為:方法區(qū)、堆、虛擬機(jī)棧、本地方法棧、程序計(jì)數(shù)器幾個(gè)區(qū)域。

程序計(jì)數(shù)器

因?yàn)?Java 程序是多線程的,CPU可以在多個(gè)線程中分配執(zhí)行時(shí)間片段。(多線程運(yùn)行時(shí),多個(gè)線程執(zhí)行需要靠 CPU 搶占資源來執(zhí)行),所以 JVM 中設(shè)計(jì) 程序計(jì)數(shù)器 的作用就是為了記錄代碼執(zhí)行的位置。

程序計(jì)數(shù)器的作用

當(dāng)某一個(gè)線程被 CPU 掛起時(shí),需要記錄代碼已經(jīng)執(zhí)行到的位置,方便 CPU 重新執(zhí)行此線程時(shí),知道從哪行開始執(zhí)行。

【程序計(jì)數(shù)器】是虛擬機(jī)中一塊較小的內(nèi)存空間,主要用于記錄當(dāng)前程序執(zhí)行的位置。

程序計(jì)數(shù)器執(zhí)行流程

上圖展示了程序計(jì)數(shù)器在 CPU 中的作用,每個(gè)線程都會(huì)記錄當(dāng)前代碼執(zhí)行的位置,當(dāng)下一次該線程繼續(xù)執(zhí)行時(shí),會(huì)從程序計(jì)數(shù)器記錄的位置繼續(xù)往下執(zhí)行。除了順序執(zhí)行外:分支操作、循環(huán)操作、跳轉(zhuǎn)、異常處理等都需要依賴程序計(jì)數(shù)器來完成。

關(guān)于程序計(jì)數(shù)器的幾點(diǎn)注意:
  1. 在 Java 虛擬機(jī)規(guī)范中,對(duì)程序計(jì)數(shù)器這一區(qū)域沒有規(guī)定任何 OutOfMemoryError 情況(或許沒有必要)
  2. 程序計(jì)數(shù)器是線程私有的,每條線程內(nèi)部都有一個(gè)私有程序計(jì)數(shù)器,它的生命周期隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結(jié)束而死亡。
  3. 當(dāng)一個(gè)線程正在執(zhí)行一個(gè) Java 方法的時(shí)候,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址,如果正在執(zhí)行的是 Native 方法,這個(gè)計(jì)數(shù)器值則為空(Underined)

虛擬機(jī)棧

虛擬機(jī)棧 是線程私有的,與線程的生命周期同步。

在 Java 虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常:

StackOverflowError:當(dāng)線程請(qǐng)求棧深度超出虛擬機(jī)棧所允許的深度時(shí)拋出。

OutOfMemoryError:當(dāng) Java 虛擬機(jī)動(dòng)態(tài)擴(kuò)展到無法申請(qǐng)足夠內(nèi)存時(shí)拋出。

學(xué)習(xí)JVM時(shí)會(huì)經(jīng)常看到這一句話:【JVM 是基于棧的解釋器執(zhí)行的,DVM 是基于寄存器解釋器執(zhí)行的】

上面的 基于棧 指的就是虛擬機(jī)棧。虛擬機(jī)棧的初衷是用來描述 Java 方法執(zhí)行的內(nèi)存模型,每個(gè)方法被執(zhí)行的時(shí)候,JVM 都會(huì)在虛擬機(jī)棧中創(chuàng)建一個(gè) 棧幀

棧幀

棧幀(Stack Frame)是用于支持虛擬機(jī)進(jìn)行方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),每一個(gè)線程在執(zhí)行某個(gè)方法時(shí),都會(huì)為這個(gè)方法創(chuàng)建一個(gè)棧幀。

一個(gè)線程包含多個(gè)棧幀(因?yàn)闀?huì)執(zhí)行多個(gè)方法,每個(gè)方法都會(huì)創(chuàng)建一個(gè)棧幀),而每個(gè)棧幀內(nèi)部包含:局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、返回地址。如下圖展示:

棧幀示意圖

局部變量表

局部變量表 是變量值的存儲(chǔ)空間。調(diào)用方法時(shí)傳遞的參數(shù)以及在方法內(nèi)部創(chuàng)建的局部變量都保存在 局部變量表 中。

在 Java 編譯成 class 文件的時(shí)候,會(huì)在方法 Code 屬性表中的 max_locals 數(shù)據(jù)項(xiàng)中確定該方法需要分配的最大局部變量表的容量。

如下面示例:

public static int add(int k){
    int i = 1;
    int j = 2;
    return i + j + k;
}

將上述代碼通過 javac 命令編譯成 class 文件,再通過 javap -v 命令進(jìn)行反編譯,結(jié)果如下:

  public static int add(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: iconst_1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iload_1
         5: iload_2
         6: iadd
         7: iload_0
         8: iadd
         9: ireturn
      LineNumberTable:
        line 8: 0
        line 9: 2
        line 10: 4

可以看到 locals 中定義的數(shù)就是局部變量表的長(zhǎng)度為3。符合我們?cè)诖a中的定義。

【注意】系統(tǒng)不會(huì)為局部變量賦予初始值,不存在類變量那樣的準(zhǔn)備階段(實(shí)例變量和類變量都會(huì)被賦予初始值)

操作數(shù)棧

操作數(shù)棧(Operand Stack)也常稱為操作棧,它是一個(gè)后入先出棧(LIFO)。

操作數(shù)棧的最大深度也在編譯的時(shí)候?qū)懭敕椒ǖ?Code 屬性表中的 max_stacks 數(shù)據(jù)項(xiàng)中,棧中的元素可以是任意 Java 數(shù)據(jù)類型,包括 longdouble。

當(dāng)一個(gè)方法剛開始執(zhí)行時(shí),這個(gè)方法的操作數(shù)棧是空的,在方法執(zhí)行過程中,會(huì)有各種字節(jié)碼指令被壓入和彈出操作數(shù)棧。例如:iadd 指令就是將操作樹棧中棧頂?shù)膬蓚€(gè)元素彈出執(zhí)行加法運(yùn)算,并將結(jié)果重新壓回到操作數(shù)棧中。

動(dòng)態(tài)鏈接

動(dòng)態(tài)鏈接 主要目的為了支持方法調(diào)用過程中的動(dòng)態(tài)鏈接。

在一個(gè) class 文件中,一個(gè)方法要調(diào)用其他方法:需要將這些方法的符號(hào)引用轉(zhuǎn)化為其所在內(nèi)存地址中的直接引用,而符號(hào)引用存在于 方法區(qū) 中。

Java 虛擬機(jī)棧中,每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧所屬方法的符號(hào)引用。

返回地址

當(dāng)一個(gè)方法開始執(zhí)行后,只有兩種方式可以退出這個(gè)方法:

正常退出:方法中代碼正常完成,或者遇到一個(gè)方法返回的字節(jié)碼指令(如 return )并退出沒有拋出任何異常。

異常退出:方法執(zhí)行過程中遇到異常,并且這個(gè)異常在方法體內(nèi)部沒有得到處理,導(dǎo)致方法退出。

不管方法是 正常退出 還是 異常退出,都會(huì)返回到調(diào)用該方法的位置處。所以,虛擬機(jī)棧中的 返回地址 是用來幫助 當(dāng)前方法恢復(fù)它的上層方法執(zhí)行狀態(tài)

值得說明的是:

當(dāng) 正常退出時(shí),調(diào)用者的 PC 計(jì)數(shù)值可以作為返回地址,棧幀中可能保存此計(jì)數(shù)值。當(dāng) 異常退出時(shí),返回地址是通過異常處理器表確定的,棧幀中一般不會(huì)保存此部分信息。

實(shí)例講解
public class Hello {
    public static int add(){
        int i = 1;
        int j = 2;
        int result = i + j;
        return result + 10;
    }
}

將上述代碼使用 javap 命令來查看字節(jié)碼指令:

  public static int add();
    descriptor: ()I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=0
         0: iconst_1    // 把常數(shù) 1 壓入操作數(shù)棧棧頂
         1: istore_0    // 把操作數(shù)棧棧頂?shù)某鰲7湃刖植孔兞勘硭饕秊?1 的位置
         2: iconst_2    // 把常量 2 壓縮操作數(shù)棧棧頂
         3: istore_1    // 把操作數(shù)棧棧頂?shù)某鰲7湃刖植孔兞勘硭饕秊?2 的位置
         4: iload_0     // 把局部變量表索引為 1 的值放入操作數(shù)棧棧頂
         5: iload_1     // 把局部變量表索引為 2 的值放入操作數(shù)棧棧頂
         6: iadd        // 將操作數(shù)棧棧頂?shù)暮蜅m斚旅娴囊粋€(gè)進(jìn)行加法運(yùn)算后放入棧頂
         7: istore_2    // 把操作數(shù)棧棧頂?shù)某鰲7湃刖植孔兞勘硭饕秊?2 的位置
         8: iload_2     // 把局部變量表索引為 2 的值放入操作數(shù)棧棧頂
         9: bipush  10  // 把常量 10 壓入操作數(shù)棧棧頂
        11: iadd        // 將操作數(shù)棧棧頂?shù)暮蜅m斚旅娴囊粋€(gè)進(jìn)行加法運(yùn)算后放入棧頂
        12: ireturn     // 結(jié)束

指令詳解:

iconstbipush 將常量壓入操作數(shù)棧頂,區(qū)別是:當(dāng) int 取值為 -1 ~ 5 采用 iconst 指令,取值 -128 ~ 127 采用 bipush 指令。

istore 將操作數(shù)棧頂?shù)脑胤湃刖植孔兞勘淼哪乘饕恢?。比?istore_5 代表將操作數(shù)棧頂元素放入局部變量表下標(biāo)為 5 的位置。

iload 將局部變量表中某下標(biāo)上的值加載到操作數(shù)棧頂中。比如 iload_2 代表將局部變量表索引為 2 上的值壓入操作數(shù)棧頂。

iadd 代表加法運(yùn)算,具體是將操作數(shù)棧最上方的兩個(gè)元素進(jìn)行相加操作,然后將結(jié)果重新壓入棧頂。

上述的下標(biāo)操作的邏輯是:在 **.java 被編譯成 **.class的時(shí)候,棧幀中需要多大的局部變量表、多深的操作數(shù)棧都已經(jīng)完全確定了,并且寫入到了方法表的 Code 屬性中。

本地方法棧

本地方法棧和虛擬機(jī)棧基本相同,是針對(duì)本地(Native)方法,在開發(fā)中如果涉及 JNI 可能接觸本地方法棧多一些,在有些虛擬機(jī)的實(shí)現(xiàn)中已經(jīng)將兩個(gè)合二為一了(比如:HotSpot)。

堆(Heap)

是 JVM 所管理的內(nèi)存中最大的一塊區(qū)域,該區(qū)域唯一目的就是存放對(duì)象實(shí)例。它是 Java 垃圾回收器(GC)管理的主要區(qū)域,有時(shí)候也叫做“GC堆”。同時(shí)是所有線程共享的內(nèi)存區(qū)域,被分配在此區(qū)域的對(duì)象如果被多個(gè)線程訪問,需要考慮線程安全問題。

堆示意圖

方法區(qū)

方法區(qū)是 JVM 規(guī)范里規(guī)定的一塊運(yùn)行時(shí)數(shù)據(jù)區(qū)。

方法區(qū)主要是存儲(chǔ):

  • 已經(jīng)被 JVM 加載的類信息(版本、字段、方法、接口)
  • 常量
  • 靜態(tài)變量
  • 即時(shí)編譯器編譯后的代碼
  • 數(shù)據(jù)

方法區(qū)是被各個(gè)線程共享的內(nèi)存區(qū)域。

方法區(qū)與永久區(qū):

方法區(qū)是 JVM 規(guī)范中規(guī)定的一塊區(qū)域,但是并不是實(shí)際實(shí)現(xiàn),切忌將規(guī)范和實(shí)現(xiàn)混為一談,不同的 JVM 廠商可以有不同的版本的“方法區(qū)”實(shí)現(xiàn)。

例如:HotSpot 在 JDK1.7 以前使用 “永久區(qū)”(或者叫Perm區(qū))來實(shí)現(xiàn)方法區(qū),在 JDK1.8 后“永久區(qū)”就已經(jīng)被移除了,取而代之的是一個(gè)叫做“元空間(metaspace)”的實(shí)現(xiàn)方式。

【總結(jié)】

方法區(qū):是規(guī)范層面的東西,規(guī)定了這一個(gè)區(qū)域要存放哪些數(shù)據(jù)。

永久區(qū)或者metaspace:是對(duì)方法區(qū)的不同實(shí)現(xiàn),是實(shí)現(xiàn)層面的東西。

異常

StackOverflowError 棧溢出異常

遞歸調(diào)用是造成 StackOverflowError 的一個(gè)常見場(chǎng)景。

public class StackOver {
    private int number;
    
    public static void main(String[] args){
        StackOver so = new StackOver();
        try {
            so.method();
        } catch(StackOverflowError e){
            System.out.println("棧容量已經(jīng)溢出!");
        }
    }
    
    public void method(){
        number++;
        method();
    }
}

每調(diào)用一次 method 方法,都會(huì)在虛擬機(jī)棧中創(chuàng)建出一個(gè)棧幀,因?yàn)槭沁f歸調(diào)用,method 方法并不會(huì)退出,也不會(huì)將棧幀銷毀。

OutOfMemoryError 內(nèi)存溢出異常

理論上,虛擬機(jī)棧、堆、方法區(qū)都有發(fā)生 OutOfMemoryError 的可能,但在實(shí)際項(xiàng)目中,大多發(fā)生在堆中:

public class HeapError {
    public static void main(String[] args){
        ArrayList list = new ArrayList();
        while (true) {
            list.add(new HeapError());
        }
    }
}

總結(jié)

上面說的 JVM 運(yùn)行時(shí)內(nèi)存5種布局只是 Java虛擬機(jī)規(guī)范中定義的規(guī)則,并不是虛擬機(jī)的具體實(shí)現(xiàn)。虛擬機(jī)的具體實(shí)現(xiàn)有很多:如 HotSpot、JRocket、IBM J9、Dalvik、ART等。

JVM示意圖




最后編輯于
?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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