jvmmemorymodel
這篇文章主要介紹在jvm規范中描述的運行時數據區(runtimedataareas)。這些區域設計用來存儲被jvm自身或者在jvm上運行的程序所是用的數據。
我們先總覽jvm,然后介紹下字節碼,最后介紹不同的數據區域。
總覽
jvm作為操作系統的抽象,保證同樣的代碼在不同的硬件或操作系統上的行為一致。
比如:
對于基本類型int,無論在16位/32位/64位操作系統上,都是一個32位有符號整數。范圍從-2^31到2^31-1
無論操作系統或者硬件是大字節序還是小字節序,保證jvm存儲和使用的內存中的數據都是大字節序(先讀高位字節)
不同的jvm實現可能會有些區別,但大體上是相同的。
上圖是一個jvm的總覽
jvm解釋編譯器生成的字節碼。雖然jvm是java虛擬機的縮寫,但是只要是能夠編譯為字節碼的語言,都可以基于jvm運行,比如 scala、groovy<喎?"/kf/ware/vc/" target="_blank" class="keylink">vcd4ncjxwps6qwcux3mpixrw3sbxetmxfzekvt6os19a92slru+gxu2nsyxnzbg9hzgvyvnpu2lkiu7q05rw91mvq0mqxyv2+3cf41tc1xnk7upbh+npyo6zwqrxavnpu2mv8tcrjbgfzc2xvywrlcrg7z/q72bvy1d9kvk3no9a51mvq0kgjpc9wpg0kpha+vnpu2lxe19a92slrzai5/da00nds/cfmkgv4zwn1dglvbiblbmdpbmupvfjq0l3iys26zda00na8l3a+dqo8cd7wtndq0v3h5tdo0qq05rsis8zq8snpz8loxkosscji57pm0plwtndqtb3exnk70ncjrlvy1d/k/b7dvmbl47xe1tc85l3hufs8l3a+dqo8cd7wtndq0v3h5tkyulru8lsmwo3t67xxsuoy2df3z7xns7xevbu7ptwvcd4ncjxwpioquty24epwtba8yrxp1shlvltksbhg0uu5pstckepjvd1qdxn0igluihrpbwupoanksvs+zcrhsng+rboj1rtq0lxetprc6yjiylxjtprc6ymx4nlrs8mxvrxytprc6yhoyxrpdmugq29kzsmho7tmt8vksvsx4nlryfqzybt6wuu1xmf40/kzxs6qpc9wpg0kpha+tprc67u6tobh+chdb2rlienhy2gpoao8tmqxseds67y8yvuosklukby2tpo1xmzhun/by0pwtbxe0nte3coqpc9wpg0kpggyiglkpq=="基于棧stack的架構">基于棧(stack)的架構
jvm使用基于棧的架構。雖然棧對于開發者是透明的,但是棧對于生成的字節碼和jvm都有很重要的作用或者說影響。
我們開發的程序,會轉換位低級別的操作,存于字節碼中。在jvm中通過操作數(operand)映射到操作指令。按照jvm規范,操作指令需要的參數是從操作數棧獲得的(the operand stack)。
舉個兩個數相加的例子。這這個操作稱為 iadd 。下面是在字節碼中 3+4 的過程
首先把3和4壓入操作數棧
調用 iadd 指令
iadd 指令會從操作數棧頂彈出2個數
3+4的結果壓入操作數棧,供后面使用
這種方式被稱為基于棧的架構。還有其他的方式可以處理低級別操作,比如基于寄存器的架構(register based architecture)。
字節碼
java字節碼是java源碼轉換為一系列低級別操作的結果。每個操作由一個字節長度的操作碼(opcode or operation code)和零或多個字節長度的參數(但是大多數操作使用的參數都是通過操作數棧獲取的)組成。一個字節可以表示256個數,從0x00到0xff,目前到java8,共使用了204個。
下面列出不同種類的字節碼操作碼以及其范圍和簡單的描述
constants: 將常量池的值或者已知的值壓入操作數棧。 0x00 - 0x14
loads: 將局部變量值壓入操作數棧。 0x15 - 0x35
stores: 從操作數棧加載值賦給局部變量 0x36 - 0x56
stack: 處理操作數棧 0x57 - 0x5f
math: 從操作數棧獲取值進行基本的數學計算 0x60 - 0x84
conversions: 進行類型之間的轉換 0x85 - 0x 93
comaprisons: 兩個值的比較操作 0x94 - 0xa6
controls: 執行goto、return、循環等等控制操作 0xa7 - 0xb1
references: 執行分配對象或數組,獲取或檢查 對象、方法、靜態方法的引用。也可以調用靜態方法。 0xb2 - oxc3
extended: extended: operations from the others categories that were added after. from value 0xc4 to 0xc9
(這句說不好什么意思。。。)
reserved: jvm實現內部是用的槽子0xca,oxfe,oxff
這204個操作都很簡單,舉幾個例子
ifeq(0x99) 判斷兩個值是否相等
iadd(0x60) 把兩個數相加
i2l (0x85) 把一個int 轉換位 long
arraylength (0xbe) 返回數組長度
pop (0x57) 從操作數棧頂彈出一個值
我們需要編譯器來創建字節碼文件,標準的java編譯器就是jdk中的 javac。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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”的字節碼文件。字節碼文件是2進制的,我們可以通過javap,把二進制的字節碼文件轉換成文本形式
java -verbose test.class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
|
classfile /c:/tmp/test. class last modified 1 avr. 2015 ; size 367 bytes md5 checksum adb9ff75f12fc6ce1cdde22a9c4c7426 compiled from "test.java" public class com.codinggeek.jvm.test sourcefile: "test.java" minor version: 0 major version: 51 flags: 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_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 3 : 0 public static void main(java.lang.string[]); flags: acc_public, acc_static code: stack= 2 , locals= 4 , args_size= 1 0 : iconst_1 1 : istore_1 2 : bipush 15 4 : istore_2 5 : iload_1 6 : iload_2 7 : invokestatic # 2 // method add:(ii)i 10 : istore_3 11 : return linenumbertable: line 6 : 0 line 7 : 2 line 8 : 5 line 9 : 11 public static int add( int , int ); flags: acc_public, acc_static code: stack= 2 , locals= 3 , args_size= 2 0 : iload_0 1 : iload_1 2 : iadd 3 : istore_2 4 : iload_2 5 : ireturn linenumbertable: line 12 : 0 line 13 : 4 } |
可以看出字節碼不只是java代碼的簡單翻譯,它包括:
類的常量池(cosntant pool)描述。常量池是用于存儲類元數據的jvm數據區域,比如類內部的方法名,參數列表,等等。當jvm加載一個類的時候,這些元數據就會加載到常量池
通過行號表和或局部變量表來提供函數和天貓的變量在字節碼中的具體位置信息
java代碼的翻譯(包括隱藏的父類構造)
提供更具體的對于操作數棧的操作和更完整的傳遞和獲取參數的方式
下面是一個簡單的字節碼文件存儲信息的描述
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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內部使用。
堆(heap)
堆 在jvm啟動的時候創建,由所有的jvm線程所共享。所有的類實例、數組都分配到堆(由new所創建的)。
堆必須由一個垃圾收集器來管理,垃圾收集器負責釋放被開發者所創建,并且不會再被使用到的對象。
至于垃圾收集的策略由jvm實現決定(比如hotspot提供了多種算法).
堆內存有一個最大值限制,如果超過這個值 jvm會拋出一個 outofmemroy異常
方法區(method area)
方法區也是被jvm的所有線程所共享。同樣的隨jvm啟動被創建。方法區存儲的數據由classloader從字節碼中加載,這些數據會在應用運行過程中一致存在,除非加載它們的classloader被銷毀或者jvm停止。
方法區存儲如下數據:
類信息(屬性名、方法名、父類名、借口名、版本、等等)
方法和構造的字節碼
加載每個類時創建的運行時常量池
jvm規范并不強迫在堆中實現方法區。在java7以前,hotspot 使用一個稱為永久帶(permgen)的區域實現方法區。永久帶與堆相鄰(和堆一樣進行內存管理),默認位64mb
從java8開始,hptspot使用分離的本地內存實現方法區,起名元數據區(metaspace)。元數據區最大可用空間即整個系統的可用內存。
如果方法去申請不到可用內存,jvm也會拋出outofmemoryerror.
運行時常量池(runtime constant pool)
運行時常量池是方法區的一部分。因為運行吃常量池對于元數據的重要性,java規范中在方法區之外單獨對其進行了描述。運行時常量池會隨著加載的類和接口而增長。
常量池有點想傳統語言中的語法表。換句話說,當調用一個類、方法或屬性時,jvm通過運行時常量池來尋找這些數據在內存中的真實地址。運行時常量池也包含字符串字面值或基本類型的常量
1
2
3
|
stirng mystring= "this is a string litteral" static final int my_constant = 2 ; |
pc(程序計數器)寄存器(每個線程) the pc register (per thread)
每個線程有自己的pc(程序計數器)寄存器,與線程創建是一同創建。每個線程在一個時間點上只能執行一個方法,稱為該線程的當前方法(current method)。pc寄存器包含jvm當前在執行指令(在方法區)的地址。
如果當前執行的方法是本地方法(native),pc寄存器的值是undefined
虛擬機棧每個線程-java-virtual-machine-stacks-per-thread">虛擬機棧(每個線程) java virtual machine stacks (per thread)
虛擬機棧存儲多個幀,因此在描述棧前,我們先來看下幀
幀(frames)
幀是一個數據結構,幀包含表示線程正在執行的當前方法狀態的多個數據:
操作數棧(operand stack): 之前已經提到過,字節碼指令使用操作數棧來傳遞參數
局部變量數組(local variable array): 這個數組包含當前執行方法的一個作用域內的所有局部變量。這個數組可以包含基本類型、引用或者返回地址。局部變量數組的大小在編譯時就已經確定。jvm在方法調用時使用局部變量傳遞參數,被調方法的局部變量數組通過調用方法的操作數棧創建。
運行時常量池引用: 引用當前類當前被執行方法的常量池。jvm使用常量池引用傳遞信號給真正的內存引用。
棧(stack)
每個jvm線程都有一個私有的jvm棧,與線程同時創建。java虛擬機棧存儲幀。每次調用一個方法時,都會創建一個幀,并且壓入虛擬機棧。當這個方法執行完成時,這個幀也會銷毀(無論方法是正常執行完成,還是拋出異常)
在一個線程執行的過程中只有一個幀是可用的。這個幀稱為當前幀(current frame)。
對局部變量和操作數棧的操作通常和當前幀的引用一起。
我們再看一個加法的例子
1
2
3
4
5
6
7
8
9
|
public int add( int a, int b){ return a + b; } public void functiona(){ // some code without function call int result = add( 2 , 3 ); //call to function b // some code without function call } |
在方法a內部,a幀是當前幀,位于虛擬機棧頂。在調用add方法開始時,創建一個新的幀b,并且壓入虛擬機棧。幀b成為新的當前幀。
幀b的局部變量數組通過幀a的操作數棧中的數據填充。當add方法結束在,幀b被銷毀,幀a重新成為當前幀。add方法的結果壓入a幀的操作數棧,這樣方法a可以通過幀a的操作數棧獲取add 的結果.
總結
以上就是本文關于java虛擬機運行時數據區分析的全部內容,希望對大家有所幫助。如有不足之處,歡迎留言指出。
原文鏈接:https://www.2cto.com/kf/201608/543147.html