目錄
1、什麼是運行時數據區
恩......如果我說什麼是Java的內存模型,估計大家都會脫口而出,程序計數器、方法棧、本地方法棧、元空間、堆。而且很自信的覺得自己說的一點都沒錯。are you really?
其實對於java的內存模型和運行時數據區很多同學都搞不清楚。java的內存模型是JMM(Java Memory Model,簡稱 JMM )是定義了線程和主內存之間的抽象關係,即 JMM 定義了 JVM 在計算機內存(RAM)中的工作方式,如果我們要想深入瞭解Java併發編程,就要先理解好Java內存模型。可以見我的併發編程專題文章,併發編程十二-Java內存模型以及底層實現原理 瞭解JAVA的內存模型。
程序計數器、方法棧、本地方法棧、元空間、堆這些統稱爲java運行區域,也叫運行時數據區。
2、運行時數據區介紹
2.1 程序計數器
程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。
由於 Java 虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器內核都只會執行一條線程中的指令。
因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類內存區域爲“線程私有”的內存。
如果線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是 Native 方法,這個計數器值則爲空(Undefined)。此內存區域是唯一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 情況的區域。
2.2虛擬機棧
與程序計數器一樣,Java 虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。
虛擬機棧描述的是 Java 方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀(Stack Frame,是方法運行時的基礎數據結構)用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。如下圖
1. 局部變量表
局部變量表是存放方法參數和局部變量的區域。 局部變量沒有準備階段, 必須顯式初始化。如果是非靜態方法,則在 index[0] 位置上存儲的是方法所屬對象的實例引用,一個引用變量佔 4 個字節,隨後存儲的是參數和局部變量。字節碼指令中的 STORE 指令就是將操作棧中計算完成的局部變呈寫回局部變量表的存儲空間內。
虛擬機棧規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出 StackOverflowError 異常;如果虛擬機棧可以動態擴展(當前大部分的 Java 虛擬機都可動態擴展),如果擴展時無法申請到足夠的內存,就會拋出 OutOfMemoryError 異常。
2. 操作棧
操作棧是個初始狀態爲空的桶式結構棧。在方法執行過程中, 會有各種指令往棧中寫入和提取信息。JVM 的執行引擎是基於棧的執行引擎, 其中的棧指的就是操作棧。字節碼指令集的定義都是基於棧類型的,棧的深度在方法元信息的 stack 屬性中。
我們可以通過Javap -v 命令,查看每個函數執行的順序和指令。
如以下這個程序,我們看下i++和++i在程序裏具體是怎麼進行執行的。
public class test {
public static int sum(int i) {
i = i++;
return i;
}
public static int sum2(int i) {
i = ++i;
return i;
}
public static void main(String[] args) {
System.out.println(sum(1));
System.out.println(sum(2));
}
}
將程序使用javac變異爲class文件,然後在執行
javap -v test.class >test
j將文件導出,然後查看test文件內容。大家肯定一臉懵逼。。。。哈哈,不要急,拿出武林祕籍,JAVA指令集,所有的反編譯指令都已經總結道那篇博客裏了。大家可以當做字典使用。然後我們對照着指令,查看具體執行流程吧。
Last modified Jun 1, 2020; size 549 bytes
MD5 checksum 2aaae1ab4ce8152ade04b4ca65c34a2f
Compiled from "test.java"
public class com.dahua.test.controller.test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."<init>":()V
#2 = Fieldref #19.#20 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #5.#21 // com/dahua/test/controller/test.sum:(I)I
#4 = Methodref #22.#23 // java/io/PrintStream.println:(I)V
#5 = Class #24 // com/dahua/test/controller/test
#6 = Class #25 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 sum
#12 = Utf8 (I)I
#13 = Utf8 sum2
#14 = Utf8 main
#15 = Utf8 ([Ljava/lang/String;)V
#16 = Utf8 SourceFile
#17 = Utf8 test.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = Class #26 // java/lang/System
#20 = NameAndType #27:#28 // out:Ljava/io/PrintStream;
#21 = NameAndType #11:#12 // sum:(I)I
#22 = Class #29 // java/io/PrintStream
#23 = NameAndType #30:#31 // println:(I)V
#24 = Utf8 com/dahua/test/controller/test
#25 = Utf8 java/lang/Object
#26 = Utf8 java/lang/System
#27 = Utf8 out
#28 = Utf8 Ljava/io/PrintStream;
#29 = Utf8 java/io/PrintStream
#30 = Utf8 println
#31 = Utf8 (I)V
{
public com.dahua.test.controller.test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 16: 0
public static int sum(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0 //2、從局部變量0中裝載int類型值 爲1
1: iinc 0, 1 //3、把一個常量值加到一個int類型的局部變量上 爲2
4: istore_0 //4、將int類型值存入局部變量0 將2存在0的位置上
5: iload_0 //5、從局部變量0中裝載int類型值 讀取2
6: ireturn //返回
LineNumberTable:
line 18: 0
line 19: 5
public static int sum2(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iinc 0, 1 //2、把一個常量值加到一個int類型的局部變量上
3: iload_0 //3、從局部變量0中裝載int類型值
4: istore_0 //4、將int類型值存入局部變量0
5: iload_0 //5、從局部變量0中裝載int類型值
6: ireturn
LineNumberTable:
line 23: 0
line 24: 5
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iconst_1 //1、將int類型常量1壓入棧
4: invokestatic #3 // Method sum:(I)I
7: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iconst_2
14: invokestatic #3 // Method sum:(I)I
17: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
20: return
LineNumberTable:
line 28: 0
line 29: 10
line 30: 20
}
i++ 和 ++i 的區別:
- i++:從局部變量表取出 i 並壓入操作棧,然後對局部變量表中的 i 自增 1,將操作棧棧頂值取出使用,最後,使用棧頂值更新局部變量表,如此線程從操作棧讀到的是自增之前的值。
- ++i:先對局部變量表的 i 自增 1,然後取出並壓入操作棧,再將操作棧棧頂值取出使用,最後,使用棧頂值更新局部變量表,線程從操作棧讀到的是自增之後的值。
之前之所以說 i++ 不是原子操作,即使使用 volatile 修飾也不是線程安全,就是因爲,可能 i 被從局部變量表(內存)取出,壓入操作棧(寄存器),操作棧中自增,使用棧頂值更新局部變量表(寄存器更新寫入內存),其中分爲 3 步,volatile 保證可見性,保證每次從局部變量表讀取的都是最新的值,但可能這 3 步可能被另一個線程的 3 步打斷,產生數據互相覆蓋問題,從而導致 i 的值比預期的小。
對於操作數棧中的指令,大家可以通過javap 命令將java class程序編譯下,就可以看到實際中程序的每個指令的執行順序。
3. 動態鏈接
每個棧幀中包含一個在常量池中對當前方法的引用, 目的是支持方法調用過程的動態連接。
4.方法返回地址
方法執行時有兩種退出情況:
- 正常退出,即正常執行到任何方法的返回字節碼指令,如 RETURN、IRETURN、ARETURN 等;
- 異常退出。
無論何種退出情況,都將返回至方法當前被調用的位置。方法退出的過程相當於彈出當前棧幀,退出可能有三種方式:
- 返回值壓入上層調用棧幀。
- 異常信息拋給能夠處理的棧幀。
- PC計數器指向方法調用後的下一條指令。
棧指向堆
當我們局部變量表中存在Object對象的時候,因爲對象存儲是在堆中的,所以就會出現棧指向堆的場景
2.3本地方法棧
本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧爲虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。Sun HotSpot 虛擬機直接就把本地方法棧和虛擬機棧合二爲一。與虛擬機棧一樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常。
線程開始調用本地方法時,會進入 個不再受 JVM 約束的世界。本地方法可以通過 JNI(Java Native Interface)來訪問虛擬機運行時的數據區,甚至可以調用寄存器,具有和 JVM 相同的能力和權限。 當大量本地方法出現時,勢必會削弱 JVM 對系統的控制力,因爲它的出錯信息都比較黑盒。對內存不足的情況,本地方法棧還是會拋出 nativeheapOutOfMemory。
JNI 類本地方法最著名的應該是 System.currentTimeMillis()
,JNI使 Java 深度使用操作系統的特性功能,複用非 Java 代碼。 但是在項目過程中, 如果大量使用其他語言來實現 JNI , 就會喪失跨平臺特性。
2.4 方法區
方法區(Method Area)與 Java 堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然
Java 虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做 Non-Heap(非堆),目的應該是與 Java 堆區分開來。
Java 虛擬機規範對方法區的限制非常寬鬆,除了和 Java 堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。垃圾收集行爲在這個區域是比較少出現的,其內存回收目標主要是針對常量池的回收和對類型的卸載。當方法區無法滿足內存分配需求時,將拋出 OutOfMemoryError 異常。
JDK8 之前,Hotspot 中方法區的實現是永久代(Perm),JDK8 開始使用元空間(Metaspace),以前永久代所有內容的字符串常量移至堆內存,其他內容移至元空間,元空間直接在本地內存分配。
爲什麼要使用元空間取代永久代的實現?
- 字符串存在永久代中,容易出現性能問題和內存溢出。
- 類及方法的信息等比較難確定其大小,因此對於永久代的大小指定比較困難,太小容易出現永久代溢出,太大則容易導致老年代溢出。
- 永久代會爲 GC 帶來不必要的複雜度,並且回收效率偏低。
- 將 HotSpot 與 JRockit 合二爲一。
類信息、常量、靜態變量、JIT
2.5 heap(堆)
對於大多數應用來說,Java 堆(Java Heap)是 Java 虛擬機所管理的內存中最大的一塊。Java 堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。
堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”(Garbage Collected Heap)。從內存回收的角度來看,由於現在收集器基本都採用分代收集算法,所以 Java 堆中還可以細分爲:新生代和老年代;再細緻一點的有 Eden 空間、From Survivor 空間、To Survivor 空間等。從內存分配的角度來看,線程共享的 Java 堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB)。
Java 堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可,當前主流的虛擬機都是按照可擴展來實現的(通過 -Xmx 和 -Xms 控制)。如果在堆中沒有內存完成實例分配,並且堆也無法再擴展時,將會拋出 OutOfMemoryError 異常。
本章主要介紹了java運行時數據區的作用,下章將給大家解析堆的內存結構~