详细分析JVM内存模型
JVM內存模型
JAVA的主要特點是其著名的WOTA(write once, run anywhere):“編寫一次,隨處運行”。為了應用它,Sun Microsystems創建了Java虛擬機,這是對底層OS的抽象,它解釋了編譯的Java代碼。該JVM是JRE(Java運行環境)的核心組件,創建運行Java代碼,但現在所使用的其他語言有(Scala、Groovy、JRuby)。
在本文中,我將重點介紹JVM規范中描述的Runtime Data Areas。這些區域旨在存儲程序或JVM本身使用的數據。首先,我將概述JVM,然后是什么字節碼,并以不同的數據區域結尾。
JVM整體概況
JVM是底層OS的抽象。它確保無論JVM在什么硬件或操作系統上運行,相同的代碼都將以相同的行為運行。例如:
-
無論JVM是在16位/ 32位/ 64位OS上運行的,原始類型int的大小始終是從-2 ^ 31到2 ^ 31-1的32位有符號整數。
-
無論底層的OS /硬件是big-endian還是little-endian,每個JVM都以大端順序(高字節在前)存儲和使用內存中的數據。
注意:有時,一個JVM實現的行為與另一個不同,但通常是相同的。
該圖概述了JVM: -
JVM 解釋由編譯類的源代碼產生的字節碼。盡管術語JVM代表“ Java虛擬機”,但它可以運行其他語言,例如scala或groovy,只要它們可以編譯成Java字節碼即可。
-
為了避免磁盤I / O,字節碼由運行時數據區之一中的類加載器加載到JVM 中。此代碼將保留在內存中,直到JVM停止運行或銷毀(加載了它的)類加載器為止。
-
然后,加載的代碼由執行引擎解釋并執行。
-
執行引擎需要存儲數據,例如指向正在執行的代碼的指針的指針。它還需要存儲- 在開發人員代碼中處理的數據。
-
執行引擎還負責處理底層操作系統。
注意:許多JVM實現的執行引擎不是總是解釋字節碼,而是將字節碼編譯為本地代碼(如果經常使用的話)。它稱為即時(JIT)編譯,可大大加快JVM的速度。編譯后的代碼是臨時保存在通常稱為“代碼緩存”的區域中的 。由于該區域不在JVM規范中,因此在本文的其余部分中將不再討論。
基于堆棧的架構
JVM使用基于堆棧的體系結構。盡管它對于開發人員是不可見的,但它對生成的字節碼和JVM體系結構具有巨大的影響,這就是為什么我將簡要解釋該概念。
JVM通過執行Java字節碼中描述的基本操作來執行開發人員的代碼(我們將在下一章中看到)。操作數是指令對其進行操作的值。根據JVM規范,這些操作要求參數通過稱為操作數棧的棧傳遞。
例如,讓我們對2個整數進行基本加法。該操作被稱為ADD。如果要在字節碼中添加3和4:
- 他首先在操作數堆棧中壓入3和4。
- 然后調用add指令。
- add將彈出操作數堆棧的最后2個值。
- 將int結果(3 + 4)壓入操作數堆棧,以供其他操作使用。
這種功能方式稱為基于堆棧的體系結構。還有其他處理基本操作的方法,例如,基于寄存器的體系結構將操作數存儲在較小的寄存器中,而不是堆棧中。臺式機/服務器(x86)處理器和以前的android虛擬機Dalvik使用基于寄存器的體系結構。
字節碼
由于JVM會解釋字節碼,因此在深入了解字節碼之前很有用。
Java字節碼是轉換為一組基本操作的Java源代碼。每個操作由代表執行指令的一個字節(稱為操作碼或操作代碼)以及用于傳遞參數的零個或多個字節組成(但大多數操作使用操作數堆棧來傳遞參數)。在256種可能的1字節長的 操作碼(十六進制從0x00到0xFF的值)中,204在Java8規范中正在使用。
這是字節碼操作的不同類別的列表。對于每個類別,我都添加了一個簡短的描述和操作碼的十六進制范圍:
- 常量:用于將常量池中的值(我們將在后面介紹)或將已知值中的值推入操作數堆棧中。從值0x00到0x14
- 加載:用于將局部變量的值加載到操作數堆棧中。從值0x15到0x35
- 存儲:用于從操作數堆棧存儲到局部變量。從值0x36到0x56
- 堆棧:用于處理操作數堆棧。從值0x57到0x5f
- Math:對操作數堆棧中的值進行基本數學運算。從值0x60到0x84
- 轉換:用于從一種類型轉換為另一種類型。從值0x85到0x93
- 比較:用于兩個值之間的基本比較。從值0x94到0xa6
- 控制:諸如goto,return等基本操作,允許進行更高級的操作,例如循環或返回值的函數。從值0xa7到0xb1
- 引用:用于分配對象或數組,獲取或檢查對象,方法或靜態方法的引用。也用于調用(靜態)方法。從值0xb2到0xc3
- 擴展:之后添加的其他類別的操作。從值0xc4到0xc9
- 保留:供每個Java虛擬機實現內部使用。3個值:0xca,0xfe和0xff。
這204個操作非常簡單,例如:
- 操作數ifeq(0x99)檢查2個值是否相等
- add操作數(0x60)將2個值相加
- 操作數2l(0x85)將整數轉換為long
- 操作數arraylength(0xbe)給出數組的大小
- 操作數pop(0x57)從操作數堆棧中彈出第一個值
要創建字節碼,需要一個編譯器,JDK中包含的標準java編譯器是javac。
讓我們看一下簡單的添加:
public class Test {public static void main(String[] args) {int a =1;int b = 15;int result = add(a,b);}public static int add(int a, int b){int result = a + b;return result;} }“ javac Test.java”命令在Test.class中生成一個字節碼。由于Java字節碼是二進制代碼,因此人類無法讀取。Oracle在其JDK javap中提供了一個工具,該工具可以將二進制字節碼轉換為JVM規范中易于閱讀的帶有 標簽的操作碼集。
命令“ javap -verbose Test.class”給出以下結果:
Classfile /C:/TMP/Test.classLast modified 1 avr. 2015; size 367 bytesMD5 checksum adb9ff75f12fc6ce1cdde22a9c4c7426Compiled from "Test.java" public class com.codinggeek.jvm.TestSourceFile: "Test.java"minor version: 0major version: 51flags: ACC_PUBLIC, ACC_SUPER Constant pool:#1 = Methodref #4.#15 // java/lang/Object."<init>":()V#2 = Methodref #3.#16 // com/codinggeek/jvm/Test.add:(II)I#3 = Class #17 // com/codinggeek/jvm/Test#4 = Class #18 // java/lang/Object#5 = Utf8 <init>#6 = Utf8 ()V#7 = Utf8 Code#8 = Utf8 LineNumberTable#9 = Utf8 main#10 = Utf8 ([Ljava/lang/String;)V#11 = Utf8 add#12 = Utf8 (II)I#13 = Utf8 SourceFile#14 = Utf8 Test.java#15 = NameAndType #5:#6 // "<init>":()V#16 = NameAndType #11:#12 // add:(II)I#17 = Utf8 com/codinggeek/jvm/Test#18 = Utf8 java/lang/Object {public com.codinggeek.jvm.Test();flags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnLineNumberTable:line 3: 0public static void main(java.lang.String[]);flags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: iconst_11: istore_12: bipush 154: istore_25: iload_16: iload_27: invokestatic #2 // Method add:(II)I10: istore_311: returnLineNumberTable:line 6: 0line 7: 2line 8: 5line 9: 11public static int add(int, int);flags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=3, args_size=20: iload_01: iload_12: iadd3: istore_24: iload_25: ireturnLineNumberTable:line 12: 0line 13: 4 }可讀的.class表明字節碼不僅僅包含Java源代碼的簡單轉錄。它包含了:
- 類的常量池的描述。常量池是JVM的數據區域之一,用于存儲有關類的元數據,例如方法的名稱,其參數……在JVM中加載類時,此部分將進入常量池。
- 諸如LineNumberTable或LocalVariableTable之類的信息,用于指定函數的位置(以字節為單位)及其字節碼中的變量。
- 開發人員的Java代碼(加上隱藏的構造函數)的字節碼形式。
- 處理操作數堆棧的特定操作,以及更廣泛地傳遞和獲取參數的方式。
僅供參考,這是對存儲在.class文件中的信息的簡要說明:
ClassFile {u4 magic;u2 minor_version;u2 major_version;u2 constant_pool_count;cp_info constant_pool[constant_pool_count-1];u2 access_flags;u2 this_class;u2 super_class;u2 interfaces_count;u2 interfaces[interfaces_count];u2 fields_count;field_info fields[fields_count];u2 methods_count;method_info methods[methods_count];u2 attributes_count;attribute_info attributes[attributes_count]; }運行時數據區
運行時數據區是旨在存儲數據的內存中區域。這些數據由開發人員的程序或JVM用于內部工作。
此圖顯示了JVM中不同運行時數據區域的概述。每個線程的某些區域是唯一的。
堆
堆是在所有Java虛擬機線程之間共享的內存區域。它是在虛擬機啟動時創建的。所有類實例和數組都在堆中分配(使用new運算符)。
MyClass myVariable = new MyClass(); MyClass[] myArrayClass = new MyClass[1024];當不再使用開發人員分配的實例時,必須由垃圾收集器 來管理該區域。清理內存的策略取決于JVM實現(例如,Oracle Hotspot提供了多種算法)。
堆可以動態擴展或收縮,并且可以具有固定的最小和最大大小。例如,在Oracle Hotspot中,用戶可以通過以下方式用Xms和Xmx參數指定堆的最小大小:“ java -Xms = 512m -Xmx = 1024m…”。
注意:堆不能超過最大限制。如果超出此限制,JVM將拋出OutOfMemoryError。
方法范圍
方法區域是所有Java虛擬機線程之間共享的內存。它是在虛擬機啟動時創建的,并由類加載器從字節碼加載。只要加載它們的類加載器處于活動狀態,方法區域中的數據就會保留在內存中。
方法區域存儲:
- 類信息(字段/方法的數量,超類名稱,接口名稱,版本等)
- 方法和構造函數的字節碼。
- 每個類加載的運行時常量池。
規范不強制在堆中實現方法區域。例如,在JAVA7之前,Oracle HotSpot使用一個名為PermGen的區域來存儲“方法區域”。該PermGen與Java堆(以及由JVM像堆一樣由JVM管理的內存)是連續的,并且被限制為默認空間64M(由參數-XX:MaxPermSize修改)。從Java 8開始,HotSpot現在將“方法區域”存儲在稱為Metaspace的分離的本機內存空間中,最大可用空間是總可用系統內存。
注意:方法區域不能超過最大限制。如果超出此限制,JVM將拋出OutOfMemoryError。
運行時常量池
該池是“方法區域”的子部分。由于它是元數據的重要組成部分,因此Oracle規范描述了“方法區域”之外的運行時常量池。對于每個加載的類/接口,此常量池都會增加。該池就像常規編程語言的符號表。換句話說,當引用一個類,方法或字段時,JVM使用運行時常量池在內存中搜索實際地址。它還包含常量值,例如字符串文字或常量圖元。
String myString1 = “This is a string litteral”; static final int MY_CONSTANT=2;pc寄存器(程序計數器)
每個線程都有自己的pc(程序計數器)寄存器,與該線程同時創建。在任何時候,每個Java虛擬機線程都在執行單個方法的代碼,即該線程的當前方法。pc寄存器包含當前正在執行的Java虛擬機指令的地址(在方法區域中)。
注意:如果線程當前正在執行的方法是本機的,則Java虛擬機的pc寄存器的值是未定義的.Java虛擬機的pc寄存器足夠寬,可以在特定平臺上保存returnAddress或本機指針。
Java虛擬機堆棧(每個線程)
堆棧區域存儲多個幀,因此在討論堆棧之前,我將介紹這些幀。
棧
棧是一種數據結構,其中包含多個數據,這些數據表示當前方法(正在調用的方法)中線程的狀態:
- 操作數堆棧:在基于堆棧的體系結構一章中,我已經介紹了操作數堆棧。字節碼指令使用此堆棧來處理參數。此堆棧還用于在(java)方法調用中傳遞參數,并在調用方法的堆棧頂部獲取被調用方法的結果。
- 局部變量數組:此數組包含當前方法范圍內的所有局部變量。該數組可以保存基本類型,引用或returnAddress的值。該數組的大小是在編譯時計算的。Java虛擬機使用局部變量在方法調用時傳遞參數,被調用方法的數組是從調用方法的操作數堆棧中創建的。
- 運行時常量池引用:引用正在執行的當前方法的當前類的常量池。JVM使用它將符號方法/變量引用(例如myInstance.method())轉換為實內存引用。
堆
每個Java虛擬機線程都有一個私有Java虛擬機堆棧,與該線程同時創建。Java虛擬機堆棧存儲框架。每次調用方法時,都會創建一個新框架并將其放入堆棧中。框架的方法調用完成時,無論該完成是正常的還是突然的(它引發未捕獲的異常),它都會被銷毀。
給定線程中的任何一點都只有一個框架(用于執行方法的框架)處于活動狀態。該幀稱為當前幀,其方法稱為當前方法。定義當前方法的類是當前類。局部變量和操作數堆棧上的操作通常參考當前幀。
讓我們看下面的示例,它是一個簡單的加法
public int add(int a, int b){return a + b; }public void functionA(){ // some code without function callint result = add(2,3); //call to function B // some code without function call }當functionA()在其上運行時,這是它在JVM內部的工作方式:
在functionA()內部,框架A是堆棧框架的頂部,并且是當前框架。在內部調用add()的開始處,將新框架(框架B)放入堆棧中。幀B成為當前幀。通過彈出幀A的操作數堆棧來填充幀B的局部變量數組。add()完成后,幀B被銷毀,幀A再次成為當前幀。add()的結果放在框架A的操作數堆棧上,以便functionA()可以通過彈出其操作數堆棧來使用它。
注意:此堆棧的功能使其可以動態擴展和收縮。有一個堆棧不能超過的最大大小,這限制了遞歸調用的數量。如果超出此限制,則JVM拋出 StackOverflowError。
使用Oracle HotSpot,可以使用參數-Xss指定此限制。
本機方法堆棧(每個線程)
這是用非Java語言編寫并通過JNI(Java本機接口)調用的本機代碼的堆棧。由于它是一個“本機”堆棧,因此該堆棧的行為完全取決于基礎操作系統。
結論
我希望本文能幫助您更好地了解JVM。我認為,最棘手的部分是JVM堆棧,因為它與JVM的內部功能緊密相關。
本文來源:JVM內存模型
總結
以上是生活随笔為你收集整理的详细分析JVM内存模型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 在技术团队中发展的7个关键技能
- 下一篇: 详细分析内部类的发生内存泄漏的原因