1 導(dǎo)讀
經(jīng)常會看到JVM內(nèi)存模型,其實(shí)說的就是JVM運(yùn)營時數(shù)據(jù)區(qū)的一個結(jié)構(gòu),這篇文章主要記錄了對不同JDK版本運(yùn)營時數(shù)據(jù)區(qū)的學(xué)習(xí)做一個總結(jié)。
2 運(yùn)行時數(shù)據(jù)區(qū)
2.1 什么是運(yùn)行時數(shù)據(jù)區(qū)
Java虛擬機(jī)在執(zhí)行程序的過程中,為了方便對程序進(jìn)行內(nèi)存管理,會將他管理的內(nèi)存區(qū)域劃分為幾個不同作用的區(qū)域。JVM運(yùn)行時數(shù)據(jù)區(qū)包含以下圖中幾個區(qū)域,其中堆、方法區(qū)是不同線程共享的,而程序計數(shù)器、本地方法棧、虛擬機(jī)棧是線程私有的。

2.2 運(yùn)行時數(shù)據(jù)區(qū)介紹
2.2.1 程序計數(shù)器
簡單來講,程序計數(shù)器是程序執(zhí)行當(dāng)前字節(jié)碼的位置信息,每一個線程都有私有的程序計數(shù)器,這樣就可以保證每個線程可以獨(dú)立運(yùn)行自己的代碼,減少與其他線程的交互,提高代碼執(zhí)行效率。
在《Java虛擬機(jī)規(guī)范》中,如果當(dāng)前執(zhí)行的不是本地方法,那么記錄的是當(dāng)前指令的地址;如果執(zhí)行的是本地方法,那么這個值是未指定的(Undefind),這塊區(qū)域很大,足以放下指令指針和本地指針。
2.2.2 虛擬機(jī)棧
每一個Java線程都有一個私有的虛擬機(jī)棧,與線程生命周期同步。當(dāng)每個方法被執(zhí)行的時候,Java虛擬機(jī)會同步創(chuàng)建一個棧幀,用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每個方法被調(diào)用到調(diào)用結(jié)束,就對應(yīng)一個棧幀從入棧到出棧。
public class JVMTest {
?
public static void main(String[] args) {
?
int i = 0;
String str = "abs";
JVMTest jvmTest = new JVMTest();
String m = jvmTest.m(str);
System.out.println(m);
}
?
private String m(String m) {
?
return m;
}
?
private static String m2(String m2) {
return m2;
}
}
-
局部變量表
局部變量表是一組變量值存儲空間,用于存放方法參數(shù)和方法內(nèi)部定義的局部變量。
下面圖片是main方法的局部變量表,第一個局部變量是形參0-args,后面依次是 1-i、2-str、3-jvmTest、4-m變量

-
操作數(shù)棧
下面是main方法的字節(jié)碼指令
0 iconst_0 #將int類型常量值0壓入棧
1 istore_1 #將int類型值存入局部變量1
2 ldc #2 <a> #把常量池中的項壓入棧
4 astore_2 #將引用類型或returnAddress類型值存入局部變量2
5 new #3 <com/hp/test/thread/JVMTest> #創(chuàng)建一個JVMTest新對象
8 dup #復(fù)制棧頂部一個字長內(nèi)容
9 invokespecial #4 <com/hp/test/thread/JVMTest.<init>> #調(diào)用初始化方法(之后會在類加載中詳細(xì)講解init方法)
12 astore_3 #將引用類型或returnAddress類型值存入局部變量3
13 aload_3 #從局部變量中裝載引用類型值
14 aload_2 #從局部變量中裝載引用類型值
15 invokespecial #5 <com/hp/test/thread/JVMTest.m> #調(diào)用m方法
18 astore 4 #將引用類型或returnAddress類型值存入局部變量4
20 getstatic #6 <java/lang/System.out> #從類中獲取靜態(tài)字段
23 aload 4 #從局部變量中裝在引用類型值
25 invokevirtual #7 <java/io/PrintStream.println> #調(diào)用println方法
28 return #從方法中返回,返回值為void
下面是m方法的字節(jié)碼指令
0 aload_1 #從局部變量1中裝載引用類型值
1 areturn #從方法中返回引用類型的數(shù)據(jù)
那么問題來了,明明方法m中形參是第一個局部變量,為什么是aload_1而不是aload_0?
這是因為如果當(dāng)前幀是由構(gòu)造方法或者實(shí)例方法創(chuàng)建的,那么該對象引用this將會存在index為0 的Slot處,非靜態(tài)方法,都會創(chuàng)建this的一個參數(shù),index為0,其余的參數(shù)是按照順序排放的,static 方法被不可以使用this是因為static方法中沒有放this的index。
再來看一下m2方法字節(jié)碼指令
0 aload_0
1 areturn
-
方法出口
通俗的講,每一個方法在執(zhí)行完或者拋出異常都需要回到上一個方法調(diào)用的地方,這就是方法出口。
-
動態(tài)鏈接
通俗的講,我們在方法調(diào)用時并不是直接將方法的信息保存在同一個棧幀中,而是指向該方法在常量池中的地址引用。
2.2.3 本地方法棧
? 本地方法棧(Native Method Stacks)與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,其區(qū)別只是虛擬機(jī) 棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機(jī)使用到的本地(Native) 方法服務(wù)。
2.2.4 方法區(qū)
? 它用于存儲已被虛擬機(jī)加載 的類型信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼緩存等數(shù)據(jù)。我們平時操作new對象時所需要的類信息就存儲在方法區(qū)。(之后會在類加載中詳細(xì)講解)
? 在JDK1.8以前,方法區(qū)存放的位置可以理解成永久代,但是到了JDK1.8,用元數(shù)據(jù)區(qū)(metaspace)取代了永久代。永久代是需要分配內(nèi)存大小的,而元數(shù)據(jù)區(qū)在不指定大小的情況下,受限于物理內(nèi)存
2.2.5 堆
? 對于Java程序來說,堆是JVM管理內(nèi)存最大的一部分,此內(nèi)存部分是用于存放對象實(shí)例。根據(jù)對象的年齡,可以分為年輕代老年代。在年輕代中又可以分為Eden區(qū),Survive區(qū),之后要講到的GC也主要是針對堆進(jìn)行展開。
3 總結(jié)
想要對JVM有更深入的理解,必須先搞清楚每個數(shù)據(jù)區(qū)的作用,以及代碼是如何在各個數(shù)據(jù)區(qū)串聯(lián)起來的。