1. JVM內存區域划分
jvm在運行java應用程序過程中,會把它所管理的內存划分為若干不同的數據區域。

☝️ 灰色部分(Java棧,本地方法棧和程序計數器)是線程私有,不存在線程安全問題,橙色部分(方法區和堆)為線程共享區。
2. 類加載器
類加載器(Class Loader)負責加載class文件,class文件在文件開頭有特定的文件標識,將class文件字節碼內容加載到內存中,並將這些內容轉換成方法區中的運行時數據結構。ClassLoader只負責class文件的加載,至於它是否可以運行,則由執行引擎Execution Engine決定。類加載示意圖:
也就是說,類加載器識別的class文件除了是.class格式外,文件的開頭還得有特殊的標識,使用文本編輯器打開一個class格式的文件:
cafe babe 0000 0034 0010 0a00 0300 0d07
000e 0700 0f01 0006 3c69 6e69 743e 0100
0328 2956 0100 0443 6f64 6501 000f 4c69
6e65 4e75 6d62 6572 5461 626c 6501 0012
4c6f 6361 6c56 6172 6961 626c 6554 6162
6c65 0100 0474 6869 7301 0014 4c63 632f
6d72 6269 7264 2f63 6173 2f54 6573 743b
0100 0a53 6f75 7263 6546 696c 6501 0009
5465 7374 2e6a 6176 610c 0004 0005 0100
1263 632f 6d72 6269 7264 2f63 6173 2f54
6573 7401 0010 6a61 7661 2f6c 616e 672f
4f62 6a65 6374 0021 0002 0003 0000 0000
0001 0001 0004 0005 0001 0006 0000 002f
0001 0001 0000 0005 2ab7 0001 b100 0000
0200 0700 0000 0600 0100 0000 0300 0800
0000 0c00 0100 0000 0500 0900 0a00 0000
0100 0b00 0000 0200 0c
這個特定的標識就是十六進制字符cafe babe。
3. 程序計數器
🌳 程序計數器是一塊非常小
的內存空間,它可以看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時通過改變程序計數器的值來選取下一條需要執行的字節碼指令。讀取一個指令后,將該指令“翻譯”成固定的操作,並根據這些操作進行分支、循環、跳轉、異常處理等流程。
🌳 JVM的多線程實現方式是通過CPU時間片輪轉(即線程輪流切換並分配處理器執行時間)算法來實現的。也就是說,某個線程在執行過程中可能會因為時間片耗盡而被掛起,而另一個線程獲取到時間片開始執行。當被掛起的線程重新獲取到時間片的時候,它要想從被掛起的地方繼續執行,就必須知道它上次執行到哪個位置,在JVM中,通過程序計數器來記錄某個線程的字節碼執行位置。因此,每個線程工作時都有屬於自己的獨立計數器。各個線程之間的計數器互不影響,獨立存取,這類內存區域成為 線程私有 內存
🌳如果執行的是一個Native方法,那這個計數器的值為undefied。
通過一段代碼,我們來看一下程序計數器所記錄的字節碼的行號
-
新建Test.java文件
public class Test { public static void main(String[] args) { int a = 1; int b = 1; int sum = a + b; System.out.println(sum); } }
-
編譯Test.java為字節碼文件
javac Test.java
-
使用javap工具打開字節碼文件
javap -verbose Test.class # 右側行號,左側為指令 stack=2, locals=4, args_size=1 0: iconst_1 1: istore_1 2: iconst_1 3: istore_2 4: iload_1 5: iload_2 6: iadd 7: istore_3 8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 11: iload_3 12: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 15: return
假如當前線程的程序計數器存儲的指令地址為6,這時候CPU切換到別的線程中處理工作;一段時間后,當前線程重新獲取了CPU時間片繼續執行時,根據程序計數器存的6就知道,當前需要執行iadd(即a+b操作)指令。執行引擎會將這條指令翻譯為機器指令,然后CPU執行該運算操作。
4. 虛擬機棧(Java棧)
🌳 虛擬機棧也稱為Java棧,每個方法被執行的時候,Java虛擬機都會同步創建一個棧幀(Stack Frame)。
- Java虛擬機棧是線程私有的,它的生命周期與線程相同(隨線程而生,隨線程而滅)。
- 棧幀包括局部變量表、操作數棧、動態鏈接、方法返回地址和一些附加信息。
- 每一個方法被調用直至執行完畢的過程,就對應這一個棧幀在虛擬機棧中從入棧到出棧的過程。
虛擬機棧示意圖如下所示:
棧幀結構:
本地方法棧
🌳 本地方法棧(Native Method Stacks)與虛擬機棧發揮的作用的非常相似的。虛擬機棧是為虛擬機執行java方法服務,而本地方法棧是為虛擬機執行本地方法服務的。
什么是本地方法接口?
本地方法接口(Native Interface)的作用是融合不同的編程語言為Java所用,它的初衷是融合C/C++程序。Java誕生的時候是 C/C++橫行的時候,要想立足,必須有調用 C/C++程序,於是就在內存中專門開辟了一塊區域處理標記為native的代碼。例如查看java.lang.Thread類中存在許多native方法:
public static native void yield();
public static native void sleep(long millis) throws InterruptedException;
native方法沒有方法體(因為不是Java實現),所以看上去像是“接口”一樣,故得名本地方法接口。
Java堆
Java 堆是虛擬機所管理的內存中最大的一塊,是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一作用就是存放對象實例,幾乎所有的對象實例都是在這里分配的(不絕對,在虛擬機的優化策略下,也會存在棧上分配、標量替換的情況)。當類加載器讀取了類文件后,需要把類、方法、常量、變量放到堆內存中,保存所有引用類型的真實信息,以方便執行器執行。
Java 堆是 GC 回收的主要區域,因此很多時候也被稱為 GC 堆。從內存回收的角度看,Java 堆還可以被細分為新生代和老年代;再細一點新生代還可以被划分為 Eden Space、From Survivor Space、To Survivor Space。從內存回收的角度看,線程共享的 Java 堆可能划分出多個線程私有的分配緩沖區(Thread Local Allocation Buffer,TLAB)。「屬於線程共享的內存區域」
堆在邏輯上分為三個區域:
Java7:
Java8:
可以看到,在Java7時代,堆分為新生區(新生區包含伊甸園區和幸存區,幸存區又包含幸存者0區和幸存者1區。此外,幸存者0區又稱為From區,幸存者1區又稱為To區,From區和To區並不是固定的,復制之后交互,誰空誰是To),養老區和永久代;在Java8中,永久代已經被移除,被一個稱為元空間的區域所取代。元空間的本質和永久代類似。
元空間與永久代之間最大的區別在於:永久代使用的JVM的堆內存(但是邏輯上是非堆的),但是java8以后的元空間並不在虛擬機中而是使用本機物理內存(所以在上圖中,我用虛線表示)。
永久代:是一個常駐內存的區域,用於存放JDK自身所攜帶的Class,Interface的元數據,即存儲的是運行環境必須的類信息,被轉載進此區域的數據是不會被垃圾回收的,只有關閉JVM才會釋放此區域所占用的內存空間。
元空間:取代永久代,不在Java虛擬機的堆中實現,而是使用本機物理內存實現。默認情況下元空間大小僅受本地內存限制。類的元數據放入native memory,字符串常量在Java堆中(運行時常量和基本類型常量在元空間——方法區)
PS:jdk1.8,jvm把字符串常量池移到了堆內存里。此時方法區=元空間
堆之所以要分區是因為:Java程序中不同對象的生命周期不同,70%~99%對象都是臨時對象,這類對象在新生區“朝生夕死”。如果沒有分區,GC時搜集垃圾需要對整個堆內存進行掃描;分區后,回收這些“朝生夕死”的對象,只需要在小范圍的區域中(新生區)搜集垃圾。所以,分區的唯一理由就是為了優化GC性能。
方法區
🌳方法區(Method Area)並不是所謂的存儲方法的區域,而是供各線程共享的運行時內存區域。它存儲了已被虛擬機加載的類信息、方法信息、字段信息、常量(被final修飾)、靜態變量、即時編譯器編譯后的代碼緩存等。
方法區也是一種規范,在不同虛擬機里頭實現是不一樣的,最典型的實現就是HotSpot虛擬機Java8之前的永久代(PermGen space)和Java8的元空間(Metaspace)。
可參考:https://www.cnblogs.com/code-duck/p/13577103.html
執行引擎
類加載器加載的字節碼並不能夠直接運行在操作系統之上,因為字節碼指令不是本地機器指令,執行引擎(Execute Engine)的任務就是講字節碼指令解釋為對應平台上的本地機器指令。通俗地講,執行引擎就是將高級語言翻譯為本地機器語言的翻譯官。
解釋器和JIT編譯器
- 解釋器(Interpreter):JVM在程序運行時通過解釋器逐行將字節碼轉為本地機器指令執行;
- JIT編譯器(Just In Time Compiler,即時編譯器):解釋器的優點是程序一啟動就可以馬上發揮作用,逐行翻譯字節碼執行程序。而對於一些高頻的代碼(如循環體內代碼和高頻調用方法等),如果每次執行都用解釋器逐行將字節碼翻譯為機器指令的話,勢必會造成浪費,所以我們可以通過即時編譯器將這部分高頻代碼直接編譯為機器指令然后緩存在方法區中(上面介紹方法區內部組成時提到過JIT代碼緩存),以此提高執行效率。和解釋器相比,即時編譯器的缺點就是編譯需要耗費一定時間。