虛擬機執行子系統 一、類文件結構 1.魔數和class版本 1.magic-魔數:0xCAFEBABE;4位元組 2.minor_version:次版本,丶之後的數字;2位元組 3.major_version:主版本,丶之前的數字;2位元組 2.常量池 1.constant_pool_count:常量池常 ...
虛擬機執行子系統
一、類文件結構
1.魔數和class版本
1.magic-魔數:0xCAFEBABE;4位元組
2.minor_version:次版本,丶之後的數字;2位元組
3.major_version:主版本,丶之前的數字;2位元組
2.常量池
1.constant_pool_count:常量池常量數量(= 此值 - 1):2位元組
由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2類型的數據,代表常量池容量計數值。
2.constant_pool:常量,第一位為類型位,之後的就是按照各自常量的定義:n位元組
3.訪問標識符
1.access_flags:訪問標識
這個標誌用於識別一些類或者介面層次的訪問信息,包括:這個Class是類還是介面;是否定義為public類型;是否定義為abstract類型;如果是類的話,是否被聲明為final等。
如:0x0001 0x0020說明是一個公共的類
4.類索引、父類索引、介面索引:Class文件中由這三項數據來確定這個類的繼承關係
1.this_class:類索引:2位元組
類索引用於確定這個類的全限定名
2.super_class:父類索引:2位元組
父類索引用於確定這個類的父類的全限定名。由於Java語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object之外,所有的Java類都有父類,因此除了java.lang.Object外,所有Java類的父類索引都不為0
3.interfaces:介面索引:2位元組數組
介面索引集合就用來描述這個類實現了哪些介面,這些被實現的介面將按implements語句(如果這個類本身是一個介面,則應當是extends語句)後的介面順序從左到右排列在介面索引集合中
介面索引開頭為數量:2位元組
查找:
類索引、父類索引和介面索引集合都按順序排列在訪問標誌之後,類索引和父類索引用兩個u2類型的索引值表示,它們各自指向一個類型為CONSTANT_Class_info的類描述符常量,通過CONSTANT_Class_info類型的常量中的索引值可以找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字元串
5.欄位表集合
可以包括的信息有:
欄位的作用域(public、private、protected修飾 符)、是實例變數還是類變數(static修飾符)、可變性(final)、併發可見性(volatile修飾 符,是否強制從主記憶體讀寫)、可否被序列化(transient修飾符)、欄位數據類型(基本類型、對象、數組)、欄位名稱。上述這些信息中,各個修飾符都是布爾值,要麼有某個修飾符,要麼沒有,很適合使用標誌位來表示。
欄位名、欄位數據類型,這些都是無法固定的,只能引用常量池中的常量來描述
1.access_flags:欄位訪問標識:u2
2.name_index:欄位簡單名稱:u2
引用常量池常量
3.descriptor_index:方法描敘符:u2
描敘欄位:欄位類型
描敘方法:(參數列表)描敘符
引用常量池常量
6.方法表集合
7.class、欄位、方法等的屬性表
預定義的有21種,每種都有自己的結構
1.Code屬性
attribute_name_index是一項指向CONSTANT_Utf8_info型常量的索引,常量值固定為“Code”,它代表了該屬性的屬性名稱
attribute_length指示了屬性值的長,所以屬性值的長度固定為整個屬性表長度減去6個位元組。
max_stack代表了操作數棧(Operand Stacks)深度的最大值。在方法執行的任意時刻,操作數棧都不會超過這個深度。虛擬機運行的時候需要根據這個值來分配棧幀(StackFrame)中的操作棧深度。
max_locals代表了局部變數表所需的存儲空間。
code_length和code用來存儲Java源程式編譯後生成的位元組碼指令。
code:位元組碼
exception:異常表
如果當位元組碼在第start_pc行[1]到第end_pc行之間(不含第end_pc行)出現了類型為catch_type或者其子類的異常(catch_type為指向一個CONSTANT_Class_info型常量的索引)
則轉到第handler_pc行繼續處理。當catch_type的值為0時,代表任意異常情況都需要轉向到handler_pc處進行處理
//Java源碼 public int inc(){ int x; try{ x=1; return x; }catch(Exception e){ x=2; return x; }finally{ x=3; } }/ /編譯後的ByteCode位元組碼及異常表 public int inc(); Code: Stack=1,Locals=5,Args_size=1 0:iconst_1//try塊中的x=1 1:istore_1 2:iload_1//保存x到returnValue中,此時x=1 3:istore 4 5:iconst_3//finaly塊中的x=3 6:istore_1 7:iload 4//將returnValue中的值放到棧頂,準備給ireturn返回 9:ireturn 10:astore_2//給catch中定義的Exception e賦值,存儲在Slot 2中 11:iconst_2//catch塊中的x=2 12:istore_1 13:iload_1//保存x到returnValue中,此時x=2 14:istore 4 16:iconst_3//finaly塊中的x=3 17:istore_1 18:iload 4//將returnValue中的值放到棧頂,準備給ireturn返回 20:ireturn 21:astore_3//如果出現了不屬於java.lang.Exception及其子類的異常才會走到這裡 22:iconst_3//finaly塊中的x=3 23:istore_1 24:aload_3//將異常放置到棧頂,並拋出 25:athrow Exception table: from to target type 0 5 10 Class java/lang/Exception 0 5 21 any 10 16 21 any
異常表實際上是Java代碼的一部分,編譯器使用異常表而不是簡單的跳轉命令來實現Java異常及finally處理機制
2.Exception屬性
方法的異常
number_of_exceptions:表示方法可能拋出number_of_exceptions種受查異常
exception_index_table:一個指向常量池中CONSTANT_Class_info型常量的索引,代表了該受查異常的類型
3.LineNumberTable屬性
LineNumberTable:描述Java源碼行號與位元組碼行號(位元組碼的偏移量)之間的對應關係它並不是運行時必需的屬性,但預設會生成到Class文件之中
可以在Javac中分別使用-g:none或-g:lines選項來取消或要求生成這項信息。如果選擇不生成LineNumberTable屬性
對程式運行產生的最主要的影響就是當拋出異常時,堆棧中將不會顯示出錯的行號,並且在調試程式的時候,也無法按照源碼行來設置斷點
line_number_table:數量為line_number_table_length、類型為line_number_info的集合,
line_number_info:表包括了start_pc和line_number兩個u2類型的數據項,前者是位元組碼行號,後者是Java源碼行號
4.LocalVariableTable屬性
LocalVariableTable:描述棧幀中局部變數表中的變數與Java源碼中定義的變數之間的關係,它也不是運行時必需的屬性
但預設會生成到Class文件之中,可以在Javac中分別使用-g:none或-g:vars選項來取消或要求生成這項信息。
如果沒有生成這項屬性,最大的影響就是當其他人引用這個方法時,所有的參數名稱都將會丟失,IDE將會使用諸如arg0、arg1之類的占位符代替原有的參數名,這對程式運行沒有影響,但是會對代碼編寫帶來較大不便,而且在調試期間無法根據參數名稱從上下文中獲得參數值
local_variable_info:代表了一個棧幀與源碼中的局部變數的關聯
start_pc和length:分別代表了這個局部變數的生命周期開始的位元組碼偏移量及其作用範圍覆蓋的長度,兩者結合起來就是這個局部變數在位元組碼之中的作用域範圍。
name_index和descriptor_index:指向常量池中CONSTANT_Utf8_info型常量的索引,分別代表了局部變數的名稱以及這個局部變數的描述符。
index:這個局部變數在棧幀局部變數表中Slot的位置。當這個變數數據類型是64位類型時(double和long),它占用的Slot為index和index+1兩個
泛型的支持屬性
在JDK 1.5引入泛型之後,LocalVariableTable屬性增加了一個“姐妹屬性”:LocalVariableTypeTable
這個新增的屬性結構與LocalVariableTable非常相似,僅僅是把記錄的欄位描述符的descriptor_index替換成了欄位的特征簽名(Signature),
對於非泛型類型來說,描述符和特征簽名能描述的信息是基本一致的,但是泛型引入之後,由於描述符中泛型的參數化類型被擦除掉
描述符就不能準確地描述泛型類型了,因此出現了LocalVariableTypeTable。
5.SourceFile屬性
SourceFile屬性用於記錄生成這個Class文件的源碼文件名稱。這個屬性也是可選的,可以分別使用Javac的-g:none或-g:source選項來關閉或要求生成這項信息。
在Java中,對於大多數的類來說,類名和文件名是一致的,但是有一些特殊情況(如內部類)例外。如果不生成這項屬性,當拋出異常時,堆棧中將不會顯示出錯代碼所屬的文件名
sourcefile_index:指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源碼文件的文件名
6.ConstantValue屬性
ConstantValue屬性的作用是通知虛擬機自動為靜態變數賦值。
只有被static關鍵字修飾的變數(類變數)才可以使用這項屬性。
對於非static類型的變數(也就是實例變數)的賦值是在實例構造器<init>方法中進行的;
而對於類變數,則有兩種方式可以選擇:
如果同時使用final和static來修飾一個變數,並且這個變數的數據類型是基本類型或者java.lang.String的話,就生成ConstantValue屬性來進行初始化
如果這個變數沒有被final修飾,或者並非基本類型及字元串,則將會選擇在<clinit>方法中進行初始化
ConstantValue屬性是一個定長屬性
attribute_length:數據項值必須固定為2。
constantvalue_index:數據項代表了常量池中一個字面量常量的引用
根據欄位類型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一種
7.InnerClasses屬性
InnerClasses屬性用於記錄內部類與宿主類之間的關聯。如果一個類中定義了內部類,那編譯器將會為它以及它所包含的內部類生成InnerClasses屬性
number_of_classes:代表需要記錄多少個內部類信息,每一個內部類的信息都由一個inner_classes_info表進行描述
inner_class_info_index和outer_class_info_index:指向常量池中CONSTANT_Class_info型常量的索引,分別代表了內部類和宿主類的符號引用。
inner_name_index:指向常量池中CONSTANT_Utf8_info型常量的索引,代表這個內部類的名稱,如果是匿名內部類,那麼這項值為0。
inner_class_access_flags:內部類的訪問標誌,類似於類的access_flags
8.Deprecated及Synthetic屬性
Deprecated屬性用於表示某個類、欄位或者方法,已經被程式作者定為不再推薦使用,它可以通過在代碼中使用@deprecated註釋進行設置。
Synthetic屬性代表此欄位或者方法並不是由Java源碼直接產生的,而是由編譯器自行添加的,
9.StackMapTable屬性
位元組碼驗證
10.Signature屬性
泛型被擦出後,獲取泛型信息
11.BootstrapMethods屬性
BootstrapMethods屬性在JDK 1.7發佈後增加到了Class文件規範之中,它是一個複雜的變長屬性,位於類文件的屬性表中。這個屬性用於保存invokedynamic指令引用的引導方法限定符。
8.位元組碼指令簡介
1.載入和存儲指令
載入和存儲指令用於將數據在棧幀中的局部變數表和操作數棧(見第2章關於記憶體區域的介紹)之間來回傳輸,這類指令包括如下內容。
將一個局部變數載入到操作棧:iload、iload_<n>、lload、lload_<n>、fload、fload_<n>、dload、dload_<n>、aload、aload_<n>。
將一個數值從操作數棧存儲到局部變數表:istore、istore_<n>、lstore、lstore_<n>、fstore、fstore_<n>、dstore、dstore_<n>、astore、astore_<n>。
將一個常量載入到操作數棧:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>、fconst_<f>、dconst_<d>。
擴充局部變數表的訪問索引的指令:wide。
2.運算指令
加法指令:iadd、ladd、fadd、dadd。
減法指令:isub、lsub、fsub、dsub。
乘法指令:imul、lmul、fmul、dmul。
除法指令:idiv、ldiv、fdiv、ddiv。
求餘指令:irem、lrem、frem、drem。
取反指令:ineg、lneg、fneg、dneg。
位移指令:ishl、ishr、iushr、lshl、lshr、lushr。
按位或指令:ior、lor。
按位與指令:iand、land。
按位異或指令:ixor、lxor。
局部變數自增指令:iinc。
比較指令:dcmpg、dcmpl、fcmpg、fcmpl、lcmp。
3.類型轉換指令
Java虛擬機直接支持(即轉換時無需顯式的轉換指令)以下數值類型的寬化類型轉換(Widening Numeric Conversions,即小範圍類型向大範圍類型的安全轉換):
int類型到long、float或者double類型。
long類型到float、double類型。
float類型到double類型。
處理窄化類型轉換(Narrowing Numeric Conversions)
這些轉換指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
4.對象訪問與創建
創建類實例的指令:new。
創建數組的指令:newarray、anewarray、multianewarray。
訪問類欄位(static欄位,或者稱為類變數)和實例欄位(非static欄位,或者稱為實例變數)的指令:getfield、putfield、getstatic、putstatic。
把一個數組元素載入到操作數棧的指令:baload、caload、saload、iaload、laload、faload、daload、aaload。
將一個操作數棧的值存儲到數組元素中的指令:bastore、castore、sastore、iastore、fastore、dastore、aastore。
取數組長度的指令:arraylength。
檢查類實例類型的指令:instanceof、checkcast。
5.操作數棧管理指令
如同操作一個普通數據結構中的堆棧那樣,Java虛擬機提供了一些用於直接操作操作數棧的指令,
包括:
將操作數棧的棧頂一個或兩個元素出棧:pop、pop2。
複製棧頂一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2。
將棧最頂端的兩個數值互換:swap
6.控制轉移指令
條件分支:ifeq、iflt、ifle、ifne、ifgt、ifge、ifnull、ifnonnull、if_icmpeq、if_icmpne、if_icmplt、if_icmpgt、if_icmple、if_icmpge、if_acmpeq和if_acmpne。
複合條件分支:tableswitch、lookupswitch。
無條件分支:goto、goto_w、jsr、jsr_w、ret。
7.方法調用和返回
invokevirtual指令用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。
invokeinterface指令用於調用介面方法,它會在運行時搜索一個實現了這個介面方法的對象,找出適合的方法進行調用。
invokespecial指令用於調用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法。
invokestatic指令用於調用類方法(static方法)。
invokedynamic指令用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法,前面4條調用指令的分派邏輯都固化在Java虛擬機內部,
invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。方法調用指令與數據類型無關,而方法返回指令是根據返回值的類型區分的,包括ireturn(當返回值是boolean、byte、char、short和int類型時使用)、lreturn、freturn、dreturn和areturn,另外還有一條return指令供聲明為void的方法、實例初始化方法以及類和介面的類初始化方法使用
8.異常處理指令
在Java程式中顯式拋出異常的操作(throw語句)都由athrow指令來實現
除了用throw語句顯式拋出異常情況之外,Java虛擬機規範還規定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。
例如,在前面介紹的整數運算中,當除數為零時,虛擬機會在idiv或ldiv指令中拋出ArithmeticException異常。
而在Java虛擬機中,處理異常(catch語句)不是由位元組碼指令來實現的(很久之前曾經使用jsr和ret指令來實現,現在已經不用了),而是採用異常表來完成的。
9.同步指令
同步一段指令集序列通常是由Java語言中的synchronized語句塊來表示的
Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義
void onlyMe(Foo f){ synchronized(f){ doSomething(); } } // 指令碼 Method void onlyMe(Foo) 0 aload_1//將對象f入棧 1 dup//複製棧頂元素(即f的引用) 2 astore_2//將棧頂元素存儲到局部變數表Slot 2中 3 monitorenter//以棧頂元素(即f)作為鎖,開始同步 4 aload_0//將局部變數Slot 0(即this指針)的元素入棧 5 invokevirtual#5//調用doSomething()方法 8 aload_2//將局部變數Slow 2的元素(即f)入棧 9 monitorexit//退出同步 10 goto 18//方法正常結束,跳轉到18返回 13 astore_3//從這步開始是異常路徑,見下麵異常表的Taget 13 14 aload_2//將局部變數Slow 2的元素(即f)入棧 15 monitorexit//退出同步 16 aload_3//將局部變數Slow 3的元素(即異常對象)入棧 17 athrow//把異常對象重新拋出給onlyMe()方法的調用者 18 return//方法正常返回 Exception table: FromTo Target Type 4 10 13 any 13 16 13 any