JVM詳解4.類文件結構


點擊進入個人博客

4.1 字節碼

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

4.2 Class類文件的結構

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

  • Class文件只有兩種數據類型:無符號數、表。
  • 無符號數:無符號數屬於基本的數據類型,以u一、u二、u四、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」是這個類的全限定名,僅僅是把類全名中的「.」替換成了「/」而已,爲了使連續的多個全限定名之間不產生混淆,在使用時最後通常會加入一個「;」表示全限定名結束。ide

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_一、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、dup二、dup_x一、dup2_x一、dup_x二、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代碼生成技術)。
相關文章
相關標籤/搜索