JVM詳解4.類文件結構


博客地址:https://spiderlucas.github.io
備用地址:http://spiderlucas.coding.me

4.1 字節碼

平臺無關:Sun公司以及其他的虛擬機提供商發佈了許多可以運行在各種不同平臺上的虛擬機,這些虛擬機都可以載入和執行同一種平臺無關的字節碼,從而實現了程序的“一次編寫,到處運行”。
語言無關:語言無關的基礎是虛擬機和字節碼存儲格式,Java虛擬機不和任何語言(包括Java)綁定,它只與Class文件這種特定的二進制文件格式所關聯,Class文件中包含了Java虛擬機指令集和符號表以及若干其他輔助信息。

4.2 Class類文件的結構

Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數據,沒有空隙存在。當遇到需要佔用8位字節以上空間的數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。

  • Class文件只有兩種數據類型:無符號數、表。
  • 無符號數:無符號數屬於基本的數據類型,以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節和8個字節的無符號數。無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。
  • :表是由多個無符號數或其他表作爲數據項構成的複合數據類型,表習慣性以_info結尾。表用於描述有層次的複合結構的數據,整個Class文件本質上就是一張表,由以下的數據項構成。
  • 容量計數器:無論是無符號數還是表,當需要描述同一類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干連續的數據項的形式。

Class文件格式

4.2.1 魔數與Class文件的版本

魔數:每個Class文件的頭4個字節稱爲魔數(Magic Number),其唯一作用是確定這個文件是否爲一個能被虛擬機接受的Class文件。值爲0xCAFEBABE。
Class的版本號:緊接着魔數的4個字節存儲的是Class的版本號——第5個和第6個字節是次版本號(Minor Version),第7個和第8個字節是主版本號(Major Version)。
版本號兼容:高版本的JDK只能向下兼容以前版本的Class文件,不能運行以後版本的Class文件。

4.2.2 常量池

常量池:緊接着主次版本號後的是常量池,也可以理解爲Class文件的資源倉庫,它是與其他項目關聯最多的數據類型,也是佔用Class文件空間最大的數據項目之一,同時還算第一個出現的表類型數據項目。
常量池計數值:由於常量池中常量數量不固定,因此在入口處要放置一項u2類型的數據,代表常量池計數值(從1開始,因爲計數的0代表“不引用任何一個常量池項目”的含義)。
常量池存放數據:常量池中主要存放兩大類常量——字面量(Literal)和符號引用(Symbolic References)。字面量比較接近於Java語言層面的常量概念——如文本字符串、聲明爲final的常量值等。符號引用則屬於編譯原理方面的概念,包括下面三類常量:類和接口的全限定名(Fully Qualified Name)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符。
動態連接:Java代碼在javac編譯的時候,並沒有連接這一步驟,而是在虛擬機加載Class文件的時候動態連接。
常量池中的項:常量池中每一項都是一個表,截止到JDK 7中更用14種各不相同的表結構數據,其共同特點就是表開始的第一位是一個u1類型的標識位。
常量池的項目類型
14中常量項的結構總表

4.2.3 訪問標誌

在常量池結束之後,緊接着的兩個字節代表訪問標誌(access_flags),這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個Class是類還是接口;是否定義爲public類型;是否定義爲abstract類型;如果是類的話,是否被聲明爲final等。
訪問標誌

4.2.4 類索引、父類索引、接口索引

類索引和父類索引:是一個u2類型的數據,用於確定這個類的全限定類名和父類的全限定類名,指向一個類型爲CONSTANT_Class_info的類描述符常量,通過CONSTANT_Class_info類型的常量中的索引類型可以找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字符串。
接口索引集合:是一組u2類型的數據集合,用於描述這個類實現了哪些接口,這些被實現的接口按照從左到右排列在接口索引集合中。入口的第一項——u2類型的數據爲接口計數器,表示索引表的容量;如果沒有實現任何接口,則該計數器爲0。

4.2.5 字段表集合

字段表:字段表(field_info)用於描述接口或者類中聲明的變量。字段(field)包括類級變量以及實例級變量,但不包括在方法內部聲明的局部變量。
一個字段包括的信息有:字段的作用域(public、private、protected修飾符)、是實例變量還是類變量(static修飾符)、可變性(final)、併發可見性(volatile修飾符,是否強制從主內存讀寫)、可否被被序列化(transient修飾符)、字段數據類型(基本類型、對象、數組)、字段名稱。
修飾符布爾值:上述這些信息中,各個修飾符都是布爾值,要麼有某個修飾符,要麼沒有,很適合使用標誌位來表示。而字段叫什麼名字、字段被定義爲什麼數據類型,這些都是無法固定的,只能引用常量池中的常量來描述。

字段表結構
類型 名稱 數量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count
字段訪問標誌
標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 字段是否public
ACC_PRIVATE 0x0002 字段是否private
ACC_PROTECTED 0x0004 字段是否protected
ACC_STATIC 0x0008 字段是否static
ACC_FINAL 0x0010 字段是否final
ACC_VOLATILE 0x0040 字段是否volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_SYNTHETIC 0x1000 字段是否由編譯器自動產生的
ACC_ENUM 0x4000 字段是否enum
name_index

name_index是對常量池的引用,代表着字段的簡單名稱。簡單名稱是指沒有類型和參數修飾的方法或者字段名稱,這個類中的inc()方法和m字段的簡單名稱分別是“inc”和“m”。
全限定名:以下面代碼爲例,“org/xxx/clazz/TestClass”是這個類的全限定名,僅僅是把類全名中的“.”替換成了“/”而已,爲了使連續的多個全限定名之間不產生混淆,在使用時最後一般會加入一個“;”表示全限定名結束。

public class TestClass {

    private int m;

    public int inc() {
        return m + 1;
    }
}
descriptor_index

descriptor_index也是對常量池的引用,代表着字段和方法的描述符。描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。根據描述符規則,基本數據類型(byte、char、double、float、int、long、short、boolean)以及代表無返回值的void類型都用一個大寫字符來表示,而對象類型則用字符L加對象的全限定名來表示。

標識字符 含義 標識字符 含義
B 基本類型byte J 基本類型long
C 基本類型char S 基本類型short
D 基本類型double Z 基本類型boolean
F 基本類型float V 特殊類型void
I 基本類型int L 對象類型,如Ljava/lang/Object

數組類型:每一維度將使用一個前置的“[”字符來描述,如一個定義爲“java.lang.String[][]”類型的二維數組,將被記錄爲:“[[Ljava/lang/String;”,,一個整型數組“int[]”被記錄爲“[I”。
描述方法:按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號“( )”之內。如方法void inc()的描述符爲“( ) V”,方法java.lang.String toString()的描述符爲“( ) LJava/lang/String;”,方法int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符爲“([CII[CIII) I”。

attributes_count與attribute_info
  • 字段表都包含的固定數據項目到descriptor_index爲止就結束了,不過在descriptor_index之後跟隨着一個屬性表集合用於存儲一些額外的信息,字段都可以在屬性表中描述零至多項的額外信息。對於本例中的字段m,他的屬性表計數器爲0,也就是說沒有需要額外描述的信息,但是,如果將字段m的聲明改爲“final static int m=123”,那就可能會存在一項名稱爲ConstantValue的屬性,其值指向常量123。
  • 字段表集合中不會列出從超類或者父接口中繼承而來的字段,但有可能列出原本Java代碼之中不存在的字段,譬如在內部類中爲了保持對外部類的訪問性,會自動添加指向外部類實例的字段。
  • 在Java語言中字段是無法重載的,兩個字段的數據類型、修飾符不管是否相同,都必須使用不一樣的名稱,但是對於字節碼來講,如果兩個字段的描述符不一致,那字段重名就是合法的

4.2.6 方法表集合

方法表的結構如同字段表一樣,依次包括了訪問標誌(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表結合(attributes)幾項,如字段表所示。

方法訪問標誌
標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 方法是否爲public
ACC_PRIVATE 0x0002 方法是否爲private
ACC_PROTECTED 0x0004 方法是否爲protected
ACC_STATIC 0x0008 方法是否爲static
ACC_FINAL 0x0010 方法是否爲final
ACC_SYNCHRONIZED 0x0020 方法是否爲synchronized
ACC_BRIDGE 0x0040 方法是否由編譯器產生的橋接方法
ACC_VARARGS 0x0080 方法是否接受不定參數
ACC_NATIVE 0x0100 方法是否爲native
ACC_ABSTRACT 0x0400 方法是否爲abstract
ACC_STRICTFP 0x0800 方法是否爲strictfp
ACC_SYNTHETIC 0x1000 方法是否由編譯器自動產生的
方法裏的代碼

方法裏的Java代碼,經過編譯器編譯成字節碼指令後,存放在方法屬性集合中一個名爲“Code”的屬性裏面,屬性表作爲Class文件格式中最具擴展性的一種數據項目。

重寫

與字段表集合相對應的,如果父類方法在子類彙總沒有被重寫(Override),方法表集合中就不會出現來自父類的方法信息。

自動添加方法

有可能會出現由編譯器自動添加的方法,最典型的便是類構造器“<clinit>”方法和實例構造器“<init>”方法。

重載

在Java語言中,要重載(Overload)一個方法,除了要與原方法具有相同的簡單名稱之外,還要求必須擁有一個與原方法不同的特徵簽名,特徵簽名就是一個方法中各個參數在常量池中的字段符號引用的集合,也就是因爲返回值不會包含在特徵簽名中,因此Java語言裏面是無法僅僅依靠返回值的不同來對一個已有方法進行重載的。但是在Class文件格式彙總,特徵簽名的範圍更大一些,只要描述符不是完全一致的兩個方法也可以共存。也就是說,如果兩個方法有相同的名稱和特徵簽名,但返回值不同,那麼也是可以合法共存於同一個Class文件中的。

4.2.7 屬性表集合

在Class文件、字段表、方法表中都可以攜帶自己的屬性表集合,以用於描述某些場景專有的信息。與Class文件中其他的數據項目要求嚴格的順序、長度和內容不同,屬性表集合的限制稍微寬鬆了一些,不再要求各個屬性表具有嚴格順序,並且只要不與已有屬性名重複,任何人實現的編譯器都可以向屬性表寫入自己定義的屬性信息,Java虛擬機運行時會忽略掉他不認識的屬性。

屬性表的結構

屬性名稱需要從常量池中引用一個CONSTANT_Utf8_info類型的常量來表示,而屬性的結構則是完全自定義的,只需要通過一個u4的長度屬性去說明屬性值所佔用的位數即可。一個符合規則的屬性表應該滿足下表所定義的結構:

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length
虛擬機規範預定義的屬性
屬性名稱 使用位置 含義
Code 方法表 Java代碼編譯成的字節碼指令
ConstantValue 字段表 final關鍵字定義的常量值
Deprecated 類、方法表、字段表 被聲明爲deprecated的方法和字段
Exceptions 方法表 方法拋出的異常
EnclosingMethod 類文件 僅當一個類爲局部類或者匿名類時才能擁有這個屬性,這個屬性用於標識這個類所在的外圍方法
InnerClasses 類文件 內部類列表
LineNumberTable Code屬性 Java源碼的行號與字節碼指令的對用關係
LocalVariableTable Code屬性 方法的局部變量描述
StackMapTable Code屬性 JDK1.6中新增的屬性,供新的類型檢查驗證器(Type Checker)檢查和處理目標方法的局部變量和操作數棧所需要的類型是否匹配
Signature 類、方法表、字段表 JDK1.5中新增的屬性,這個屬性用於支持泛型情況下的方法簽名,在Java語言中,任何類、接口、初始化方法或成員的泛型簽名如果包含了類型變量(Type Variables)或參數化類型(Parameterized Types),則Signature屬性會爲他記錄泛型簽名信息。由於Java的泛型採用擦除法實現,在爲了避免類型信息被擦出後導致簽名混亂,需要這個屬性記錄泛型中的相關信息
SourceFile 類文件 記錄源文件名稱
SourceDebugExtension 類文件 JDK 1.6中新增的屬性,SourceDebugExtension屬性用於存儲額外的調試信息,譬如在進行JSP文件調試時,無法同構Java堆棧來定位到JSP文件的行號,JSR-45規範爲這些非Java語言編寫,卻需要編譯成字節碼並運行在Java虛擬機中的程序提供了一個進行調試的標準機制,使用SourceDebugExtension屬性就可以用於存儲這個標準所新加入的調試信息
Synthetic 類、方法表、字段表 標識方法或字段爲編譯器自動生成的
LocalVariableTypeTable JDK 1.5中新增的屬性,他使用特徵簽名代替描述符,是爲了引入泛型語法之後能描述泛型參數化類型而添加
RuntimeVisibleAnnotations 類、方法表、字段表 JDK 1.5中新增的屬性,爲動態註解提供支持。RuntimeVisibleAnnotations屬性用於指明哪些註解是運行時(實際上運行時就是進行反射調用)可見的
RuntimeInVisibleAnnotations 類、方法表、字段表 JDK 1.5新增的屬性,與RuntimeVisibleAnnotations屬性作用剛好相反,用於指明哪些註解是運行時不可見的
RuntimeVisibleParameter Annotations 方法表 JDK 1.5新增的屬性,作用與RuntimeVisibleAnnotations屬性類似,只不過作用對象爲方法參數
RuntimeInVisibleAnnotations Annotations 方法表 JDK 1.5中新增的屬性,作用與RuntimeInVisibleAnnotations屬性類似,只不過作用對象爲方法參數
AnnotationDefault 方法表 JDK 1.5中新增的屬性,用於記錄註解類元素的默認值
BootstrapMethods 類文件 JDK 1.7中新增的屬性,用於保存invokedynamic指令引用的引導方法限定符
Code屬性

Code屬性是Class文件中最重要的一個屬性,如果把一個Java程序中的信息分爲代碼(Code,方法體裏面的Java代碼)和元數據(Metadata,包括類、字段、方法定義及其他信息)兩部分,那麼在整個Class文件中,Code屬性用於描述代碼,所有的其他數據項目都用於描述元數據。
Java程序方法體中的代碼經過Javac編譯器處理後,最終變爲字節碼指令存儲在Code屬性內。Code屬性出現在方法表的屬性集合之中,但並非所有的方法表都必須存在這個屬性,譬如接口或者抽象類中的方法就不存在Code屬性。如果方法表有Code屬性存在,那麼他的結構將如下表所示。

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 max_stack 1
u2 max_locals 1
u4 code_length 1
u1 code code_length
u2 exception_table_length 1
exception_info exception_table exception_table_length
u2 attributes_count 1
attribute_info attributes attributes_count
  • attribute_name_index:是一項指向CONSTANT_Utf8_info型常量的索引,常量值固定爲“Code”,他代表了該屬性的屬性名稱。
  • attribute_length:指示了屬性值的長度,由於屬性名稱索引與屬性長度一共爲6個字節,所以屬性值的長度固定爲整個屬性表長度減少6個字節。
  • max_stack:代表了操作數棧(Operand Stacks)深度的最大值。在方法執行的任意時刻,操作數棧都不會超過這個深度。虛擬機運行的時候需要根據這個值分配棧幀(Stack Frame)中的操作幀深度。
  • max_locals:代表了局部變量表所需的存儲空間。在這裏,max_locals的單位是Slot,Slot是虛擬機爲局部變量分配內存所使用的最小單位。對於byte、char、float、int、short、boolean和returnAddress等長度不超過32位的數據類型,每個局部變量佔用1個Slot,而double和long這兩種64位的數據類型則需要兩個Slot來存放。方法參數(包括實例方法中的隱藏參數“this”)、顯式異常處理器的參數(Exception Handler Parameter,就是try-catch語句中catch塊所定義的異常)、方法體中定義的局部變量都需要使用局部變量表來存放。另外,並不是在方法中用到了多少個局部變量,就把這些局部變量所佔Slot之和作爲max_locals的值,原因是局部變量表中的Slot可以重用,當代碼執行超出一個局部變量的作用域時,這個局部變量所佔的Slot可以被其他局部變量所使用,Javac編譯器會根據變量的作用域來分配Slot給各個變量使用,然後計算出max_locals的大小。
  • code_length和code:用來存儲java源程序編譯後生成的字節碼指令。code_length代表字節碼長度,code是用於存儲字節碼指令的一系列字節流。既然叫字節碼指令,那麼每個指令就是一個u1類型的單字節,當虛擬機讀取到code中的一個字節碼時,就可以對應找出這個字節碼代表的是什麼指令,並且可以知道這條指令後面是否需要跟隨參數,以及參數應當如何理解。我們知道一個u1數據類型的取值範圍爲0x00~0xFF,對應十進制的0~255,也就是一共可以表達256條指令,目前,Java虛擬機規範已經定義了其中約200條編碼值對應的指令含義。
  • 關於code_length:有一件值得注意的事情,雖然他是一個u4類型的長度值,理論上最大值可以達到2的32次方減1,但是虛擬機規範中明確限制了一個方法不允許超過65535條字節碼指令,即他實際只使用了u2的長度,如果超過這個限制,Javac編譯器也會拒絕編譯。一般來講,編寫Java代碼時只要不是刻意去編寫一個超長的方法來爲難編譯器,是不太可能超過這個最大值的限制。但是,某些特殊情況,例如在編譯一個很複雜的JSP文件時,某些JSP編譯器會把JSP內容和頁面輸出的信息歸併於一個方法之中,就可能因爲方法生成字節碼超長的原因而導致編譯失敗。
Exceptions屬性

這裏的Exceptions屬性是在方法表與Code屬性平級的一項屬性。Exceptions屬性的作用是列舉出方法中可能拋出的受查異常(Checked Exceptions),也就是說方法描述時在throws關鍵字啊後面列舉的異常。他的結構見下表。

類型 名稱 數量 類型 名稱 數量
u2 attribute_name_index 1 u2 number_of_exceptions 1
u4 attribute_length 1 u2 exception_index_table number_of_exceptions
  • number_of_exceptions:項表示方法可能拋出number_of_exceptions種受查異常
  • exception_index_table:每一種受查異常使用一個exception_index_table項表示,exception_index_table是一個指向常量池中CONSTANT_Class_info型常量的索引,代表了該受查異常的類型。
LineNumberTable屬性

LineNumberTable屬性用於描述Java源碼行號與字節碼行號(字節碼的偏移量)之間的對應關係。他並不是運行時必須的屬性,但默認生成到Class文件之中,可以在Javac中分別使用-g : none或-g : lines選項來取消或要求生成這項信息。如果選擇不生成LineNumberTable屬性,對程序運行產生的最主要的影響就是當拋出異常時,堆棧中將不會顯示出錯的行號,並且在調試程序的時候,也無法按照源碼行來設置斷點。LineNumberTable屬性的結構見下表。

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 line_number_table_length 1
line_number_info line_number_table line_number_table_length
  • line_number_table:是一個數量爲line_number_table_length、類型爲line_number_info的集合
  • line_number_info表:包括了start_pc和line_number兩個u2類型的數據項,前者是字節碼行號,後者是Java源碼行號。
LocalVariableTable屬性

LocalVariableTable屬性用於描述棧幀中局部變量表中的變量與Java源碼中定義的變量之間的關係,她也不是運行時必須的屬性,但默認會生成到Class文件之中,可以在Javac中分別使用-g : none或-g :vars選項來取消或要求生成這項信息。如果沒有生成這項屬性,最大的影響就是當前其他人引用這個方法時,所有的參數名稱都將會丟失,IDE將會使用諸如arg0、arg1之類的佔位符代替原有的參數名,這對程序運行沒有影響,但是會對代碼編寫帶來較大不便,而且在調試期間無法根據參數名稱從上下文中獲得參數值。LocalVariableTable屬性的結構見下表。

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 local_variable_table_length 1
local_variable_info local_variable_table local_variable_table_length
u2 start_pc 1
u2 length 1
u2 name_index 1
u2 descriptor_index 1
u2 index 1
  • start_pc和length:屬性分別代表了這個局部變量的生命週期開始地字節碼偏移量及其作用範圍覆蓋的長度,兩者結合起來就是這個局部變量在字節碼之中的作用域範圍。
  • name_index和descriptor_index:都是指向常量池中CONSTANT_Utf8_info型常量的索引,分別代表了局部變量的名稱以及這個局部變量的描述符。
  • index:是這個局部變量在棧幀局部變量表中Slot的位置。當這個變量數據類型是64位類型時(double和long),他佔用的Slot爲index和index+1兩個。
  • 姐妹屬性:在JDK1.5引入泛型之後,LocalVariableTable屬性增加了一個“姐妹屬性”:LocalVariableTypeTable,這個新增的屬性結構與LocalVariableTable非常相似,僅僅是吧記錄的字段描述符的descriptor_index替換成了字段的特徵簽名(Signature),對於非泛型類型來說,描述符和特徵簽名能描述的信息是基本一致的,但是泛型引入後,由於描述符中反省的參數化類型被擦除掉,描述符就不能準確的描述泛型類型了,因此出現了LocalVariableTypeTable。
SourceFile屬性

SourceFile屬性用於記錄生成這個Class文件的源碼文件名稱。這個屬性也是可選的,可以分別使用Javac的-g:none-g: source選項來關閉或要求生成這項信息。在Java中,對於大多數的類來說,類名和文件名是一致的,但是有一些特殊情況(如內部類)例外。如果不生成這項屬性,當拋出異常時,堆棧中將不會顯示出錯代碼所屬的文件名。這個屬性是一個定長的屬性,其結構見下表。

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 sourcefile_index  
  • sourcefile_index數據項:是指向常量池中CONSTANT_Utf8_info型常量的索引,常量值是源碼我呢見的文件名。
ConstantValue屬性

ConstantValue屬性的作用是通知虛擬機自動爲靜態變量賦值。只有被static關鍵字修飾的變量(類變量)纔可以使用這項屬性。
類似“int x = 123”和“static int x=123”這樣的變量定義在Java程序中是非常常見的事情,但虛擬機對這兩種變量賦值的方法和時刻都有所不同。對於非static類型的變量(也就是實例變量)的賦值是在實例構造器<init>方法中進行的;而對於類變量,則有兩種方式可以選擇:在類構造器<clinit>方法中或者使用ConstantValue屬性。目前Sun Javac編譯器的選擇是:如果同時使用final和static來修飾一個變量(按照習慣,這裏稱“常量”更貼切),並且這個變量的數據類型是基本類型或者java.lang.String的話,就生成ConstantValue屬性來進行初始化,如果這個變量沒有被final修飾,或者並非基本類型及字符串,則將會選擇在<clinit>方法中進行初始化。
雖然有final關鍵字才更符合“ConstantValue”的語義,但虛擬機規範中並沒有強制要求字段必須設置了ACC_FINAL標誌,只要求了有ConstantValue屬性的字段必須設置ACC_STATIC標誌而已,對final關鍵字的要求是javac編譯器自己加入的限制。而對ConstantValue屬性值只能限於基本類型和String,不過不認爲這是什麼限制,因爲此屬性的屬性值只是一個常量池的索引號,由於Class文件格式的常量類型中只有與基本屬性和字符串相對應的字面量,所以就算ConstantValue屬性在想支持別的類型也無能爲力。ConstantValue屬性的結構見下表。

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 constantvalue_index 1
  • ConstantValue屬性:是一個定長屬性,他的attribute_length數據項值必須固定爲2。
  • constantvalue_index數據項:代表了常量池中一個字面量常量的引用,根據字段類型的不同,字面量可以是CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_Integer_info、CONSTANT_String_info常量中的一種。
InnerClasses屬性

InnerClasses屬性用於記錄內部類與宿主類之間的關聯。如果一個類中定義了內部類,那編譯器將會爲他以及他所包含的內部類生成InnerClasses屬性。該屬性的結構見下表。

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_class 1
inner_classes_info inner_class number_of_classes
  • number_of_classes:代表需要記錄多少個內部類信息。
  • inner_classes_info表:每一個內部類的信息都由一個inner_classes_info表進行描述。inner_classes_info的結構見下表。
類型 名稱 數量
u2 inner_class_info_index 1
u2 outer_class_info_index 1
u2 inner_name_index 1
u2 inner_class_access_info 1
  • inner_name_index:是指向常量池中CONSTANT_Utf8_info型常量的索引,代表這個內部類的名稱,如果是匿名內部類,那麼這項值爲0.
  • inner_class_access_flags:是內部類的訪問標誌,類似於類的access_flags,他的取值範圍見下表。
標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 內部類是否爲public
ACC_PRIVATE 0x0002 內部類是否爲private
ACC_PROTECTED 0x0004 內部類是否爲protected
ACC_STATIC 0x0008 內部類是否爲static
ACC_FINAL 0x0010 內部類是否爲final
ACC_INTERFACE 0x0020 內部類是否爲synchronized
ACC_ABSTRACT 0x0400 內部類是否爲abstract
ACC_SYNTHETIC 0x1000 內部類是否嬪妃由用戶代碼產生的
ACC_ANNOTATION 0x2000 內部類是否是一個註解
ACC_ENUM 0x4000 內部類是否是一個枚舉
Deprecated及Synthetic屬性

Deprecated和Synthetic兩個屬性都屬於標誌類型的布爾屬性,只存在有和沒有的區別,沒有屬性值的概念。屬性的結構非常簡單,其中attribute_length數據項的值必須爲0x00000000,因爲沒有任何屬性值需要設置,見下表:

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
  • Deprecated屬性用於表示每個類、字段或者方法,已經被程序作者定位不在推薦使用,他可以通過在代碼中使用@deprecated註釋進行設置。
  • Synthetic屬性代表此字段或者方法並不是由Java源碼直接產生的,而是由編譯器自行添加的,在JDK 1.5之後,標識一個類、字段或者方法是編譯器自動產生的,也可以設置他們訪問標誌中的ACC_SYNTHETIC標誌位,其中最典型的例子就是Bridge Method。所有由非用戶代碼產生的類、方法及字段都應當至少設置Synthetic屬性和ACC_SYNTHETIC標誌位中的一項,唯一的例外是實例構造器“<init>”方法和類構造器“<clinit>”方法。

       

StackMapTable屬性

StackMapTable屬性在JDK 1.6發佈周增加到了Class文件規範中,他是一個複雜的變長屬性,位於Code屬性的屬性表,這個屬性會在虛擬機類加載的字節碼驗證階段被新類型檢查驗證器(Type Checker)使用,目的在於代替以前比較消耗性能的基於數據流分析的類型推導驗證器。
這個類型檢查驗證器最初來源於Sheng Liang爲Java ME CLDC實現的字節碼驗證器。新的驗證器在同樣能保證Class文件合法性的前提下,省略了在運行期通過數據流分析確認字節碼的行爲邏輯合法性的步驟,而是在編譯階段將一系列的驗證類型(Verification Types)直接記錄在Class文件之中,通過檢查這些驗證類型代替了類型推導過程,從而大幅提升了字節碼驗證的性能。這個驗證器在JDK 1.6中首次提供,並在JDK 1.7中強制代替原本基於類型推斷的字節碼驗證器。
StackMapTable屬性中包含零至多個棧映射棧(Stack Map Frames),每個棧映射幀都顯示或隱式的代表了一個字節碼偏移量,用於表示該執行到該字節碼時局部變量表和操作數棧的驗證類型。類型檢查驗證器會通過檢查目標方法的局部變量和操作數棧所需要的類型來確定一段字節碼指令是否符合邏輯約束。StackMapTable屬性的結構見下表。

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 number_of_entries 1
stack_map_frame stack_map_frame_entries number_of_entries

《Java虛擬機規範(Java SE 7版)》明確規定:在版本號大於或等於50.0的Class文件中,如果方法的Code屬性中沒有附帶StackMapTable屬性,那就意味着他帶有一個隱式的StackMap屬性。這個StackMap屬性的作用等同於number_of_entries值爲0的StackMapTable屬性。一個方法的Code屬性最多只能有一個StackMapTable屬性,否則將拋出ClassFormatError異常。

Signature屬性

Signature屬性在JDK 1.5發佈後增加到了Class文件規範之中,他是一個可選的定長屬性,可以出現於類、屬性表和方法表結構的屬性表中。在JDK 1.5大幅增強了Java語言的語法,在此之後,任何類、接口、初始化方法或成員的泛型簽名如果包含餓了類型變量(Type Variables)或參數化類型(Parameterized Types),則Signature屬性會爲他記錄泛型簽名信息。之所以要專門使用這樣一個屬性去記錄泛型類型,是因爲Java語言的泛型採用的是擦除法實現的僞泛型,在字節碼(Code屬性)中,泛型信息編譯(類型變量、參數化類型)之後都統統被擦除掉。使用擦除法的好處是實現簡單(主要修改Javac編譯器,虛擬機內部只做了很少的改動)、非常容易實現Backport,運行期也能夠節省一些類型所佔的內存空間。但壞處是運行期就無法像C#等有真泛型支持的語言那樣,將泛型類型與用戶定義的普通類型同等對待,例如運行期做反射時無法獲得到泛型信息。Signature屬性就是爲了彌補這個缺陷而增設的,現在Java的反射API能夠獲取泛型類型,最終的數據來源也就是這個屬性。Signature屬性的結構見下表。

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 signature_index 1

其中signature_index項的值必須是一個對常量池的有效索引。常量池在該索引處的項必須是CONSTANT_Utf8_info結構,表示類簽名、方法類型簽名或字段類型簽名。如果當前的Signature屬性是類文件的屬性,則這個結構表示類簽名,如果當前的Signature屬性是方法表的屬性,則這個結構表示方法類型簽名,如果當前Signature屬性是字段表的屬性,則這個結構表示字段類型簽名。

BootstrapMethods屬性

BootstrapMethods屬性在JDK 1.7發佈後增加到了Class文件規範之中,他是一個複雜的變長屬性,位於類文件的屬性表中。這個屬性用於保存invokedynamic指令引用的引導方法限定符。《Java虛擬機規範(Java SE 7版)》規定,如果某個類文件結構的常量池中曾經出現過CONSTANT_InvokeDynamic_info類型的常量,那麼這個類文件的屬性表中必須存在一個明確地BootstrapMethods屬性,另外,即使CONSTANT_InvokeDynamic_info類型的常量在常量池中出現過多次,類文件的屬性表中最多也只能一個BootstrapMethods屬性。BootstrapMethods屬性與JSR-292中的InvokeDynamic指令和java.lang.Invoke包關係非常密切。
目前的Javac暫時無法生成InvokeDynamic指令和BootstrapMethods屬性,必須通過一些非常規的手段才能使用到他們,也許在不久的將來,等JSR-292更加成熟一些,這種狀況就會改變。BootstrapMethods屬性的結構見下表。

類型 名稱 數量
u2 attribute_name_index 1
u4 attribute_length 1
u2 num_bootstrap_methods 1
bootstrap_method bootstrap_methods num_bootstrap_methods
  • num_bootstrap_methods:項的值給出了bootstrap_methods[]數組中的引導方法限定符的數量。
  • bootstrap_methods[]數組:的每個成員包含了一個指向常量池CONSTANT_MethodHandle結構的索引值,他代表了一個引導方法,還包含了這個引導方法靜態參數的序列(可能爲空)。
  • bootstrap_method:結構見下表。
類型 名稱 數量
u2 bootstrap_method_ref 1
u2 num_bootstrap_arguments 1
u2 bootstrap_arguments num_bootstrap_arguments
  • bootstrap_method_ref:bootstrap_method_ref項的值必須是一個對常量池的有效索引。常量池在該索引處的值必須是一個CONSTANT_MethodHandle_info結構。
  • num_bootstrap_arguments:num_bootstrap_arguments項的值給出了bootstrap_arguments[]數組成員的數量。
  • bootstrap_arguments[]:bootstrap_arguments[]數組的每個成員必須是一個對常量池的有效索引。常量池在該索引處必須是下列結構之一:CONSTANT_String_info、CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info或CONSTANT_MethodType_info。

4.3 字節碼指令

Java虛擬機的指令由一個字節長度的、代表着某種特定操作含義的數字(稱爲操作碼,Opcode)以及跟隨其後的零至多個代表此操作所需參數(稱爲操作數,Operands)而構成。由於Java虛擬機採用面向操作數棧而不是寄存器的架構,所以大多數的指令都不包含操作數,只有一個操作碼。
操作碼總數:Java虛擬機操作碼的長度爲一個字節,這意味着指令集的操作碼總數不可能超過256條
放棄操作數對齊:由於Class文件格式放棄了編譯後代碼的操作數長度對齊,這就意味着虛擬機處理那些超過一個字節數據的時候,不得不在運行時從字節中重建出具體數據的結構,如果要將一個16位長度的無符號整數使用兩個無符號字節存儲起來(將它們命名爲byte1和byte2),那他們的值應該是這樣的:

(byte1 << 8) | byte2

4.3.1 字節碼與數據類型

  • 大多數的指令都包含了其操作所對應的數據類型信息,iload指令用於從局部變量表中加載int型的數據到操作數棧中,而fload指令加載的則是float類型的數據。
  • 大部分與數據類型相關的字節碼指令,他們的操作碼助記符中都有特殊的字符來表明專門爲哪種數據類型服務:i代表對int類型的數據操作,l代表long,s代表short,b代表byte,c代表char,f代表float,d代表double,a代表reference。
  • 有一些指令的助記符中沒有明確地指明操作類型的字母,如arraylength指令,他沒有代表數據類型的特殊字符,但操作數永遠只能是一個數組類型的對象。
  • 還有一些指令如無條件跳轉指令goto則是與數據類型無關的。
  • 由於Java虛擬機的操作碼最多隻有256個,Java虛擬機的指令被設計成非完全獨立的(Java虛擬機規範中把這種特性稱爲“Not Orthogonal”,即並非每種數據類型和每一種操作都有對應的指令)。
  • 大部分的指令都沒有支持整數類型byte、char和short,甚至沒有任何指令支持boolean類型。編譯器會在編譯器或運行期將byte和short類型的數據帶符號擴展(Sign-Extend)爲相應的int類型數據,將boolean和char類型數據零位擴展(Zero-Extend)爲相應的int類型數據。與之類似,在處理boolean、byte、short和char類型的數組時,也會轉換爲使用對應的int類型的字節碼指令來處理。因此,大多數對於boolean、byte、short和char類型數據的操作,實際上都是使用相應的int類型作爲運算類型(Computational Type)

4.3.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_ml、iconst_、lconst_<l>、fconst_<f>、dconst_<d>
  • 擴充局部變量表的訪問索引的指令:
wide
  • 以尖括號結尾的(例如iload_<n>)這些指令助記符實際上是代表了一組指令(例如iload_<n>,他代表了iload_0、iload_1、iload_2和iload_3這幾條指令)。這幾組指令都是某個帶有一個操作數的通用指令的特殊形式。對於這若干組特殊指令來說,他們省略掉了顯示的操作數,不需要進行取操作數的動作,實際上操作數就隱含在指令中。除了這點之外,他們的語義與原生的通用指令完全一致(例如iload_0的語義與操作數爲0時的iload指令語義完全一致)。

4.3.3 運算指令

運算或算術指令用於對兩個操作數棧上的值進行某種特定運算,並把結果重新存入到操作棧頂。大體上算術指令可以分爲兩種:對整型數據進行運算的指令與對浮點型數據進行運算的指令。由於沒有直接支持byte、short、char和boolean類型的算術指令,對於這類數據的運算,應使用操作int類型的指令代替。整數與浮點數的算術指令在溢出和被零除的時候也有各自不同的行爲表現,所有的算術指令如下:

  • 加法指令: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。
整數運算
  • 在處理整型數據時,只有除法指令(idiv和ldiv)以及求餘指令(irem和lrem)中當出現除數爲零時會導致虛擬機拋出ArithmeticException異常,其餘任何整型數運算場景都不應該拋出運行時異常。
  • 對long類型數值進行比較時,虛擬機採用帶符號的比較方式,而
浮點數運算
  • 虛擬機在處理浮點數時必須嚴格遵循IEEE 754規範中所規定的行爲和限制。也就是說,Java虛擬機必須完全支持IEEE 754中定義的非正規浮點數值(Denormalized Floating-Point Numbers)和逐級下溢(Gradual Underflow)的運算規則。
  • 所有的運算結果都必須舍入到適當的精度,非精確的結果必須舍入爲可被表示的最接近的精確值,如果有兩種可表示的形式與該值一樣接近,將優先選擇最低有效位爲零的。
  • Java虛擬機在處理浮點數運算時,不會拋出任何運行時異常(這裏所講的是Java語言中的異常,勿與IEEE 754規範中的浮點異常互相混淆,IEEE 754的浮點異常是一種運算信號),當一個操作產生溢出時,將會使用有符號的無窮大來表示,如果某個操作結果沒有明確的數學定義的話,將會使用NaN值來表示。所有使用NaN值作爲操作數的算術操作,結果都會返回NaN。
  • 對浮點數值進行比較時(dcmpg、dcmpl、fcmpg、fcmpl),虛擬機會採用IEEE 754規範所定義的無信號比較(Nonsignaling Comparisons)方式。

4.3.4 類型轉換指令

類型轉換指令可以將兩種不同的數值類型進行相互轉換,JVM直接支持小範圍類型向大範圍類型的安全轉換,而處理大範圍類型到小範圍類型的窄化類型轉換則需要顯示地使用轉換指令來完成,這些指令包括:i2b、i2c、i2s、l2i、f2i、f2l、d2i、d2l和d2f。
窄化類型轉換會導致結果產生不同的正負號、不同的數量級、數值精度丟失的情況,但永遠不可能拋出運行時異常。

4.3.5 對象創建與訪問指令

類實例與數組都屬於對象,但是其創建與操作使用了不同的字節碼指令,指令如下:

  • 創建類實例:new
  • 創建數組:newarray, anewarray, multianewarray
  • 訪問類字段(static字段)和實例字段:getfield, putfield, getstatic, putstatic
  • 把一個數組元素加載到操作數棧:baload, caload, saload, iaload, laload, faload, etc.
  • 把一個操作數棧的值存儲到數組元素中:bastore, castore, sastore, iastore, etc.
  • 取數組長度:arraylength
  • 檢查類實例類型:instanceof, checkcast

4.3.6 操作數棧管理指令

如同操作一個普通數據結構中的堆棧那樣,Java虛擬機提供了一些用於直接操作數棧的指令,包括:

  • 將操作數棧的棧頂一個或兩個元素出棧:pop、pop2
  • 複製棧頂一個或兩個數值並將複製值或雙份的複製值重新壓入棧頂:dup、dup2、dup_x1、dup2_x1、dup_x2、dup2_x2
  • 將棧最頂端的兩個數值互換:swap

4.3.7 控制轉移指令

控制轉移指令可以讓Java虛擬機有條件或無條件的從指定的位置指令而不是控制轉移指令的下一條指令繼續執行程序,從概念模型上理解,可以認爲控制轉移指令就是在有條件或無條件的修改PC寄存器的值。控制轉移指令如下。

  • 條件分支: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。

int、reference、null指令集:在Java虛擬機中有專門的指令集用來處理int和reference類型的條件分支比較操作;爲了可以無需明顯標識一個實體值是否null,也有專門的指令用來檢測null值。
轉化成int類型:與算術運算時的規則一致,對於boolean類型、byte類型、char類型和short類型的條件分支比較操作,則會先執行相應類型的比較運算指令(dcmpg、dcmpl、fcmpg、fcmpl、lcmp),運算指令會返回一個整形值到操作數棧中,隨後再執行int類型的條件分支比較操作來完成整個分支跳轉。由於各種類型的比較最終都會轉化爲int類型的比較操作,int類型比較是否方便完善就顯得尤爲重要,所以Java虛擬機提供的int類型的條件分支指令是最爲豐富和強大的。

4.3.8 方法調用和返回指令

方法調用指令與數據類型無關,而方法返回指令是根據返回值的類型區分的,包括ireturn(當返回值是boolean、byte、char、short和int類型時使用)、lreturn、freturn、dreturn和areturn;另外還有一條return指令供聲明爲void的方法、實例初始化方法以及類和接口的類初始化方法使用。以下列舉了5條用於方法調用的指令:

  1. invokevirtual——指令用於調用對象的實例方法,根據對象的實際類型進行分派(虛方法分派),這也是Java語言中最常見的方法分派方式。
  2. invokeinterface——指令用於調用接口方法,他會在運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。
  3. invokespecial——指令用於調用一些需要特殊處理的實例方法,包括實例初始化方法、私有方法和父類方法。
  4. invokestatic——指令用於調用類方法(static方法)。
  5. invokedynamic——指令用於運算時動態解析出調用點限定符所引用的方法,並執行該方法,前面4條調用指令的分派邏輯都固化在Java虛擬機內部,而invokedynamic指令的分派邏輯是由用戶所設定的引導方法決定的。

4.3.9 異常處理指令

  • 在Java程序中顯示拋出異常的操作(throw 語句)都由athrow指令來實現
  • 除了用throw語句顯式拋出異常情況之外,Java虛擬機規範還規定了許多運行時異常會在其他Java虛擬機指令檢測到異常狀況時自動拋出。
  • 在Java虛擬機中,處理異常(catch語句)不是由字節碼指令來實現的(很久之前曾經使用jsr和ret指令來實現,現在已經不用了),而是採用異常表來完成的。

4.3.10 同步指令

Java虛擬機可以支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構是使用管程(Monitor)來支持的。

方法級的同步
  • 方法級的同步是隱式的,即無需通過字節碼指令來控制,他實現在方法調用和返回操作之中。
  • 虛擬機可以從方法常量池的方法表結構中的ACC_SYNCHRONIZED訪問標誌得知一個方法是否聲明爲同步方法。
  • 當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標誌是否被設置,如果設置了,執行線程就要求先成功持有管程,然後才能執行方法,最後當方法完成(無論是正常完成還是非正常完成)時釋放管程。
  • 在方法執行期間,執行線程持有了管程,其他任何線程都無法再獲取到同一個管程。如果一個同步方法執行期間拋出了異常,並且在方法內部無法處理此異常,那麼這個同步方法所持有的管程將在異常拋到同步方法之外時自動釋放。
同步一段指令集
  • 同步一段指令集通常是由Java語言中的synchronized語句塊來表示的。
  • Java虛擬機的指令集中有monitorenter和monitorexit兩條指令來支持synchronized關鍵字的語義,正確實現synchronized關鍵字需要Javac編譯器與Java虛擬機兩者共同協作支持。
  • 編譯器必須確保無論方法通過何種方式完成,方法中調用過的每條monitorenter指令都必須執行其對應的monitorexit指令,而無論這個方法是正常結束還是異常結束。
  • 爲了保證在方法異常完成時monitorenter和monoitorexit指令依然剋有正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器聲明可處理所有的異常,他的目的就是用來執行monitorexit指令。
信號量與管程

管程:管程可以看做一個軟件模塊,它是將共享的變量和對於這些共享變量的操作封裝起來,形成一個具有一定接口的功能模塊,進程可以調用管程來實現進程級別的併發控制。進程只能互斥得使用管程,即當一個進程使用管程時,另一個進程必須等待。當一個進程使用完管程後,它必須釋放管程並喚醒等待管程的某一個進程。在管程入口處的等待隊列稱爲入口等待隊列,由於進程會執行喚醒操作,因此可能有多個等待使用管程的隊列,這樣的隊列稱爲緊急隊列,它的優先級高於等待隊列。

信號量:信號量是一種抽象數據類型,由一個整形 (sem)變量和兩個原子操作組成:

  • P():sem減1,如果sem<0等待,否則繼續;
  • V():sem加1,如果sem<=0,說明當前有等着的進程,喚醒掛在信號量上的等待進程,可以是一個或多個 。

4.4 公有設計和私有實現

Java虛擬機規範描繪了Java虛擬機應有的共同程序存儲格式:Class文件格式以及字節碼指令集。這些內容與硬件、操作系統及具體的Java虛擬機實現之間是完全獨立的。

Java虛擬機實現必須能夠讀取Class文件並精確實現包含在其中的Java虛擬機代碼的語義,一個優秀的虛擬機實現,在滿足虛擬機規範的約束下對具體實現做出修改和優化也是完全可行的,並且虛擬機規範中明確鼓勵實現者這樣做。只要優化後Class文件依然可以被正確讀取,並且包含在其中的語義能得到完整的保持,那實現者就可以選擇任何方式去實現這些語義。

虛擬機實現者可以使用這種伸縮性來讓Java虛擬機獲得更高的性能、更低的內存消耗或者更好的可移植性,選擇哪種特性取決於Java虛擬機實現的目標和關注點是什麼。虛擬機實現的方式主要有以下兩種:

  • 將輸入的Java虛擬機代碼在加載或執行時翻譯成另外一種虛擬機的指令集。
  • 將輸入的Java虛擬機代碼在加載或執行時翻譯成宿主CPU的本地指令集(即JIT代碼生成技術)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章