Java虛擬機
注意:我們這裏說的虛擬機是所謂的高級語言虛擬機, 並不是像Vmware那樣, 完全虛擬一個硬件和操作系統出來。
此外java虛擬機上還可以運行clojure, scala , Jruby, Jptyon等語言
Java虛擬機列表:
- https://www.zhihu.com/question/29265430?sort=created
- https://my.oschina.net/yygh/blog/598187
CPU處理器與操作系統的整體叫平臺,每種CPU都有其特定的指令集,不同的操作系統支持不同CPU的指令集。語言跨平臺是編譯後的文件跨平臺 即.class文件,而不是源程序(.java文件)跨平臺。由於cpu指令集的差異所以Java虛擬機在不同平臺的實現是不一樣的。這樣就不會像彙編語言對平臺的依賴性那麼大。
虛擬機並不是Java的專利
Ruby, PHP, Python都有自己的虛擬機
爲什麼要用虛擬機?
- 跨平臺
- CPU指令集不用
- 操作系統接口不同
- 效率更高
- 相對於解釋型語言
- 抽象層次高,更容易編程
- 消除指針
- 不用管理內存
- ……
Class 二進制文件一覽
先來直觀的看一下一個class 文件的格式,使用16進制的方式來展示二進制的數據。
看起來很亂的一個個字節其實是有嚴格次序的, 這些次序就是java 虛擬機所規定的U4, U2中的 U 指的是無符號數 U4 就是4個字節, U2 就是2個字節。先簡單介紹下部分區域的作用,以後會繼續寫專題博客。常量池:的作用是存放 類名,方法名,超類,字段名等。你可能會有疑問類名爲什麼保存? C++編譯地址,而java是動態鏈接,每次都是通過名稱來找類。屬性:jvm非常重要的一部分描述方法中或字段的信息 metadata(元數據),元數據就是描述數據的數據。
魔術與版本號,常量池個數
Magic Number
- 每個class文件的頭4個字節稱爲魔術(Magic Number),它的唯一作用就是確定這個文件是否爲一個能被虛擬機接受的Class文件。
Major/Minor Version : 版本號
- 16進制
Major Version (0x34) = 52 - 常量池個數 (0x36) = 54
- 大端模式(Big-Endian):高位在前
- 00 36 vs 36 00
注意:小端模式: 低位在前。JVM用的是大段模式.物理的CPU用的可能是小端模式, 所以JVM與底層交互時地址值需要做轉換。
常量池
想象一下, 我們解析這個class的時候,該怎麼處理? 遇到了”07”,我們知道這是一個class info , 就去取後面的兩個字節當成index , 遇到01 就知道這是一個UTF8Info …,這些數值在JAVA虛擬機規範中都有定義。
注意:與JAVA中語言習慣不同的是,這個容量計數器是從1而不是0開始的,如上圖,常量池容量爲0x0036,即十進制的54,這就代表常量池中有53項常量,索引值的範圍爲1~53。
常量池例子
剛纔咱們看到了常量池中的兩項: Class Info , UTF8 Info , 這裏列出了更多的常量項, 注意我們最最常用的幾個 MethodRef, NameAndType, FieldRef注意他們之間的關係。詳細信息請參考JAVA虛擬機規範。你可能會注意到以下這些特殊字符 I : int , L : 代表類, [ : 一維數組, ()V:沒有返回值的方法。
幾種最常見的結構
CONSTANT_Fieldref_info {
u1 tag; //值爲9
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info {
u1 tag; //值爲10
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_NameAndType_info {
u1 tag; //值爲12
u2 name_index;
u2 descriptor_index;
}
注意:詳情請參見《java虛擬機規範》
訪問標誌(Access Flags) : U2
這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個Class是類還是接口;是否定義爲public類型;是否定義爲abstract類型;如果是類的話,是否被聲明爲final等。
類索引(U2) 、父類索引(U2)
思考問題:爲什麼需要記錄this class?
字段(Field)
字段表(field_info)用於描述接口或類中聲明的變量
u2 fields_count; // 有多少個字段
field_info {
u2 access_flags; // 例如是public , private 等等
u2 name_index; // 指向常量池的入口
u2 descriptor_index; //指向常量池的入口
u2 attributes_count; // 該字段的屬性有多少個
attribute_info attributes[attributes_count]; //屬性信息
}
標誌字符含義
方法(method)
例子:左邊是JVM中的信息 右邊是源代碼(參數名省略用”xx”)
(Ljava/lang/String;)V —> void( String xx)
(Ljava/lang/String;IF)V —> void ( String xx, int xx, float xx )
屬性(class 文件中最複雜的部分)
截至Java SE7, 已經有21個屬性
方法和字段都可能有屬性
- 例如:方法中有Code屬性, 字段有Constant Value屬性
屬性中可能有嵌套屬性
- Code屬性中還有Line Number Table, Local Variable Table,Stack Map Table 等屬性
- 可以自定義屬性
Constant Value
如果某字段爲靜態類型( access_flags 中包含 ACC_STATIC 標誌),
將會被分配 ConstantValue 屬性
ConstantValue_attribute {
//必須是一個對常量池的有效索引。常量池在該索引處的項必須是UTF8Info,表示字符串“ConstantValue”。
u2 attribute_name_index;
//固定爲2
u4 attribute_length;
//必須是一個對常量池的有效索引。常量池在該索引處的項給出該屬性表示的常量值, 可能的值有Constant_String, Constant_Long…….
u2 constantvalue_index;
}
Code屬性
Code_attribute {
u2 attribute_name_index; //指向常量池,應該是UTF8Info ,值爲”Code”
u4 attribute_length; //屬性長度, 不包括開始的6個字節
u2 max_stack; // 操作數棧的最大深度(注:編譯時已經確定)
u2 max_locals; // 最大局部變量表個數
u4 code_length; // 該方法的代碼長度
u1 code[code_length]; //真正的字節碼
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Code屬性中的字節碼
真正的字節碼就是一個個字節而已。但是,在執行的過程中需要把他們拆解成操作碼和操作數兩個部分, 特別需要注意的是, 每個操作碼所需要的操作數不一定一樣! , 有的沒有操作數(2A , aload_0), 有的有一個(10 , 1E -> bipush 30),有的有兩個,例如 A2 00 0E (if_icmp_ge 20)對於操作數而言, 有的就是立即數,例如goto 28 , bipush 30 , 有的是指向常量池的索引。每個操作碼的具體含義可以在《Java虛擬機規範》中找到
LineNumberTable屬性
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{
u2 start_pc; // 字節碼偏移量
u2 line_number; //行號
} line_number_table[line_number_table_length];
}
可選的變長屬性, 描述Java 源碼行號與字節碼行號(字節碼偏移量)之間的對應關係, 調試器可以使用, 屬於Code屬性
LocalVariableTable屬性
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{
u2 start_pc; //局部變量位於 [start_pc, start_pc+length) 之間
u2 length;
u2 name_index; //局部變量的名稱索引
u2 descriptor_index; //局部變量的描述符索引
u2 index; //局部變量在棧幀中的索引
} local_variable_table[local_variable_table_length];
}
是可選變長屬性,描述棧幀中局部變量表中的變量和java 源碼中定義的變量之間的關係, 屬於Code屬性
運行時結構圖
每個程序運行起來至少都有一個線程,每個線程都有一個函數幀。JVM細分了函數棧。操作數棧的作用:java是基於棧的虛擬機,例如想把兩個數進行相加的話必須把操作數A壓入棧,與操作數B壓入棧,然後把兩個數相加再壓入棧,然後再彈出棧。操作數棧與局部變量都會引用堆上的對象。常量池的引用在方法區,方法區存放了這個類的數據。
一個例子
編譯前與編譯後
執行時函數棧幀
0:aload_0 說明把局部變量表第0哥元素壓入棧
形成新的函數棧幀
先把demo棧幀的三個數copy到新棧幀的局部變量表中,然後開始執行add操作數棧。
什麼是動態鏈接?名稱與對象對應起來,方便以後的操作。
有三個概念需要清楚:
常量池(Constant Pool):常量池數據編譯期被確定,是Class文件中的一部分。存儲了類、方法、接口等中的常量,當然也包括字符串常量。
字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存儲編譯期類中產生的字符串類型數據。
運行時常量池(Runtime Constant Pool): 方法區的一部分,所有線程共享。虛擬機加載Class後把常量池中的數據放入到運行時常量池。
部分內容參考自:
《深入理解java虛擬機》
《JAVA虛擬機規範》
Wanna的博客