通過《編譯原理》的相關學習,我們知道我們編寫的Java代碼最終會被翻譯成class文件。Class文件格式是JVM自己定義的用於表示Java類的二進制字節流規範,與操作系統本身無關,該文件格式正是Java代碼一次編譯,跨平臺運行的關鍵。
class文件
其中u 表示n個無符號字節,如u4 magic 表示magic的取值用4個無符號字節表示,cp_info描述常量池的結構,field_info描述字段的數據結構,method_info描述方法的數據結構,attribute_info描述屬性的數據結構。ClassFile結構各項的含義如下:
- magic: 魔數,用於標識當前Class文件的文件格式,JVM可據此判斷該文件是否可以被解析,目前固定爲0xCAFEBABE
- minor_version, major_version:minor_version是副版本號,major_version是主版本號,這兩個版本是生成Class文件時根據編譯的JDK版本來確定的,用標識編譯時的JDK版本,常見的一個異常Unsupported
major.minor version 52.0就是因爲運行時的JDK版本低於編譯時的JDK版本,52是Java8的主版本號。 - constant_pool_count:常量池計數器,等於常量池中的成員數加1
- constant_pool:常量池,是一種表結構,包含class文件結構和子結構中引用的所有字符串常量,類或者接口名,字段名和其他常量,其有效索引範圍是1- (constant_pool_count-1)。其中類和接口名採用全限定形式,即在整個JVM中的絕對名稱,如java.lang.Object,方法名,字段名、局部變量名和形參名都採用非限定名,即在源代碼文件中使用相對名稱,如屬性名name。
- access_flags:用於表示某個類或者接口的訪問權限和屬性
- this_class:類索引,該值必須是對常量池中某個常量的一個有效索引值,該索引處的成員必須是一個CONSTANT_Class_info類型的結構體,表示這個class文件所定義的類和接口
- super_class:父類索引,同this_class,該值必須是對常量池中CONSTANT_Class_info類型常量的一個有效索引值,如果該值爲0,則只能表示java.lang.Object類,因爲該類是唯一一個沒有父類的類。
- interfaces_count:接口計數器,表示當前類或者接口的直接超接口的數量
- interfaces:接口表,是一個表結構,每個成員同this_class,必須是對常量池中CONSTANT_Class_info類型常量的一個有效索引值,其有效索引範圍爲0~interfaces_count,接口表中成員的順序與源代碼中給定的接口順序是一致的,interfaces[0]表示源代碼中最左邊的接口。
- fields_count:字段計數器,當前class文件所有字段的數量
- fields:字段表,是一個表結構,表中每個成員必須是filed_info數據結構,用於表示當前類或者接口的某個字段的完整描述,不包含從父類或者父接口繼承的字段
- methods_count:方法計數器,表示當前類方法表的成員個數
- methods:方法表,是一個表結構,表中每個成員必須是method_info數據結構,用於表示當前類或者接口的某個方法的完整描述,包含當前類或者接口定義的所有方法,如實例方法、類方法、實例初始化方法等,不包含從父類或者父接口繼承的方法
- attributes_count:屬性計數器,表示當前class文件attributes屬性表的成員個數
- attributes:屬性表,是一個表結構,表中每個成員必須是attribute_info數據結構,這裏的屬性是對class文件本身,方法或者字段的補充描述,如SourceFile屬性用於表示class文件的源代碼文件名。
// 爲是一個簡單的例子
public class ClassFileTest {
private int a = 5;
public int getA(){
return this.a;
}
public static void main(String[] args) {
new ClassFileTest().getA();
}
}
// javap -v
public class ClassFileTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#25 // ClassFileTest.a:I
#3 = Class #26 // ClassFileTest
#4 = Methodref #3.#24 // ClassFileTest."<init>":()V
#5 = Methodref #3.#27 // ClassFileTest.getA:()I
#6 = Class #28 // java/lang/Object
#7 = Utf8 a
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 LClassFileTest;
#16 = Utf8 getA
#17 = Utf8 ()I
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 SourceFile
#23 = Utf8 ClassFileTest.java
#24 = NameAndType #9:#10 // "<init>":()V
#25 = NameAndType #7:#8 // a:I
#26 = Utf8 ClassFileTest
#27 = NameAndType #16:#17 // getA:()I
#28 = Utf8 java/lang/Object
{
public ClassFileTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_5
6: putfield #2 // Field a:I
9: return
LineNumberTable:
line 1: 0
line 3: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this LClassFileTest;
public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LClassFileTest;
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: new #3 // class ClassFileTest
3: dup
4: invokespecial #4 // Method "<init>":()V
7: invokevirtual #5 // Method getA:()I
10: pop
11: return
LineNumberTable:
line 10: 0
line 11: 11
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 args [Ljava/lang/String;
}
例如#2對應類型是Fieldref,表示爲ClassFileTest.a:I.
例如#5對應類型是Methodref,表示爲ClassFileTest.getA:()I.
什麼意思?
描述符
描述符有兩種,字段描述符和方法描述符,本質就是一個基於特定規則的字符串,其中字段描述符用來表示類,實例和局部變量的類型,具體如下:
ClassFileTest.a:I 表示 ClassFileTest類中int成員變量a.
ClassFileTest.getA:()I.表示 ClassFileTest類中返回值是int的getA方法。
常量池
Java虛擬機指令不依賴類,接口,類實例或數組的運行時內存佈局,而是依賴依賴常量池表中的符號信息,常量池表中所有項都有如下通用格式:
cp_info{
u1 tag;//類型標記,用於確定後面的info的格式,tag是一個字節
u1 info[];//兩個或者多個字節,取決於tag的值
}
CONSTANT_Utf8_info:用於表示一個Utf8編碼的字符串
CONSTANT_Utf8_info{
u1 tag;//tag取值1
u2 length;//後面的byte數組的長度
u1 bytes[length];//字符串對應的byte數組數據
}
CONSTANT_Class_info:用於表示一個Java類或者接口名
CONSTANT_Class_info{
u1 tag;//tag取值7
u2 name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
}
CONSTANT_Fieldref_info:用於描述一個字段
CONSTANT_Fieldref_info{
u1 tag;//tag取值9
u2 class_index;//常量池中的有效索引,該索引處的成員必須是一個CONSTANT_Class_info結構,表示該字段所屬的類
u2 name_and_type_index;//常量池中的有效索引,該索引處的成員必須是一個CONSTANT_NameAndType_info結構,該結構用於表示一個字段或者方法描述符。
}
CONSTANT_MethodType_info:用於記錄方法的類型信息,即方法描述符
CONSTANT_MethodType_info{
u1 tag;//tag取值21
u2 name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
u2 descriptor_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
}
更多類型請參考《Java虛擬機規範8版》
字段
feild_info{
u2 access_flags;//字段標識
u2 name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
u2 descriptor_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
u2 attributes_count;//當前字段附加屬性數值
attribute_info attributes[attributes_count];//屬性表中的每個成員
}
方法
method_info{
u2 access_flags;//方法標識
u2 name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
u2 descriptor_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
u2 attributes_count;//當前字段附加屬性數值
attribute_info attributes[attributes_count];//屬性表中的每個成員
}
屬性
ClassFile、filed_info、method_info結構和Code屬性都有屬性表,所有的屬性都通過attribute_info結構表示,其通用格式如下:
attribute_info{
u2 attribute_name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
u4 attribute_length;//表示後面的info信息的字節長度
u1 info[attribute_length];//具體數據
}
Java8預定義了23種屬性(《Java虛擬機規範8版》中有介紹),例
Code:位於method_info的屬性表中,表示該方法的虛擬機指令及輔助信息,method_info中有且僅有一個Code屬性,其結構如下:
attribute_info{
u2 attribute_name_index;//對常量池的有效索引,該索引處的成員必須是一個CONSTANT_Utf8_info結構
u4 attribute_length;//表示後面的info信息的字節長度
u2 max_stack;//當前方法操作數棧的最大深度
u2 max_locals;//此方法引用局部變量表中的局部變量的個數,包含傳遞方法入參的局部變量
u4 code_length;//後面的code數組的字節長度
u1 code[code_length];//當前方法的虛擬機指令的數據
u2 exception_table_length;//後面的exception_table數組的長度;
{
u2 start_pc;
u2 end_pc;//try/catch的代碼範圍,具體來說是起止代碼對應的虛擬機指令在code數組中的索引
u2 handler_pc;//異常處理邏輯的代碼的虛擬機指令在code數組中的索引
u2 catch_type;//常量池中一個類型爲CONSTANT_Class_info的有效索引,表示捕獲的異常類型。
}exception_table[exception_table_length];//此方法的捕獲的各異常的異常處理邏輯
}
用戶在編譯源代碼文件時可以添加新的屬性,只要JVM實現能夠正確識別該屬性即可,注意用戶自定義的屬性不能使用已有預定義屬性的屬性名
虛擬機指令集
C/C++的方法會被編譯成特定於CPU架構的彙編指令,然後交由CPU逐一執行,因爲彙編指令與CPU架構是強綁定的,所以C/C++程序在執行前需要在不同CPU架構的機器上編譯一遍。Java爲了實現一處編譯,跨平臺運行的目標,在彙編指令之上引入了一個獨立於平臺的中間層,虛擬機指令,由Java虛擬機規範提供指令標準定義,由Java虛擬機廠商提供指令實現,不同平臺的Java虛擬機都遵循相同的指令集規範,從而實現跨平臺運行目標。一個方法對應的一組虛擬機指令稱爲這個方法的字節碼(byte codes)。
具體虛擬機指令集請查詢《Java虛擬機規範8版》,例:
public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this LClassFileTest;
操作碼 | 助記符 | 指令含義 | 例子操作 |
---|---|---|---|
42 | aload_0 | 將第一個引用類型本地變量推送至棧頂 | this對象放入棧頂 |
180 | getfield | 獲取指定類的實例字段,並將值壓入棧頂 | 獲取a的值 |
172 | ireturn | 從當前方法返回int | 返回a的值 |
主要參考
《hotspot實戰》
《Java虛擬機規範8版》
《Hotspot class文件和字節碼解析》