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ū)域:
上圖中:
-
HelloWorld.java會(huì)經(jīng)過編輯生成HelloWorld.class字節(jié)碼文件。 - Java 虛擬中要想訪問
HelloWorld這個(gè)類時(shí),需要通過類加載器(ClassLoader)進(jìn)行加載,將HelloWorld.class字節(jié)碼文件加載到 JVM 內(nèi)存中。 - 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ù)器在 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)注意:
- 在 Java 虛擬機(jī)規(guī)范中,對(duì)程序計(jì)數(shù)器這一區(qū)域沒有規(guī)定任何
OutOfMemoryError情況(或許沒有必要) - 程序計(jì)數(shù)器是線程私有的,每條線程內(nèi)部都有一個(gè)私有程序計(jì)數(shù)器,它的生命周期隨著線程的創(chuàng)建而創(chuàng)建,隨著線程的結(jié)束而死亡。
- 當(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ù)類型,包括 long 和 double。
當(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é)束
指令詳解:
iconst 和 bipush 將常量壓入操作數(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等。