Java 是與平臺無關(guān)的語言,這得益于 Java 源代碼編譯后生成的存儲字節(jié)碼的文件,即 Class 文件,以及 Java 虛擬機的實現(xiàn)。不僅使用 Java 編譯器可以把 Java 代碼編譯成存儲字節(jié)碼的 Class 文件,使用 JRuby 等其他語言的編譯器也可以把程序代碼編譯成 Class 文件,虛擬機并不關(guān)心 Class 的來源是什么語言,只要它符合一定的結(jié)構(gòu),就可以在 Java 中運行。Java 語言中的各種變量、關(guān)鍵字和運算符的語義最終都是由多條字節(jié)碼命令組合而成的,因此字節(jié)碼命令所能提供的語義描述能力肯定會比 Java 語言本身更強大,這便為其他語言實現(xiàn)一些有別于 Java 的語言特性提供了基礎(chǔ),而且這也正是在類加載時要進行安全驗證的原因。
Class 文件是一組以8位字節(jié)為基礎(chǔ)單位的二進制流,各個數(shù)據(jù)項目嚴格按照順序緊湊地排列在 Class 文件中,中間沒有添加任何分隔符,這使得整個 Class 文件中存儲的內(nèi)容幾乎全部都是程序運行的必要數(shù)據(jù)。根據(jù) Java 虛擬機規(guī)范的規(guī)定,Class 文件格式采用一種類似于 C 語言結(jié)構(gòu)體的偽結(jié)構(gòu)來存儲,這種偽結(jié)構(gòu)中只有兩種數(shù)據(jù)類型:無符號數(shù)和表。無符號數(shù)屬于基本數(shù)據(jù)類型,以 u1、u2、u4、u8 來分別代表 1、2、4、8 個字節(jié)的無符號數(shù)。表是由多個無符號數(shù)或其他表作為數(shù)據(jù)項構(gòu)成的符合數(shù)據(jù)類型,所有的表都習慣性地以“_info”結(jié)尾。
整個 Class 文件本質(zhì)上就是一張表,它由如下所示的數(shù)據(jù)項構(gòu)成。
從表中可以看出,無論是無符號數(shù)還是表,當需要描述同一類型但數(shù)量不定的多個數(shù)據(jù)時,經(jīng)常會使用一個前置的容量計數(shù)器加若干個連續(xù)的該數(shù)據(jù)項的形式,稱這一系列連續(xù)的摸一個類型的數(shù)據(jù)為某一類型的集合,比如,fields_count 個 field_info 表數(shù)據(jù)構(gòu)成了字段表集合。這里需要說明的是:Class 文件中的數(shù)據(jù)項,都是嚴格按照上表中的順序和數(shù)量被嚴格限定的,每個字節(jié)代表的含義,長度,先后順序等都不允許改變。
下表列出了 Class 文件中各個數(shù)據(jù)項的具體含義:
從表中可以看出,無論是無符號數(shù)還是表,當需要描述同一類型但數(shù)量不定的多個數(shù)據(jù)時,經(jīng)常會在其前面使用一個前置的容量計數(shù)器來記錄其數(shù)量,而便跟著若干個連續(xù)的數(shù)據(jù)項,稱這一系列連續(xù)的某一類型的數(shù)據(jù)為某一類型的集合,如:fields_count 個 field_info 表數(shù)據(jù)便組成了方法表集合。這里需要注意的是:Class 文件中各數(shù)據(jù)項是按照上表的順序和數(shù)量被嚴格限定的,每個字節(jié)代表的含義、長度、先后順序都不允許改變。
magic 與 version
每個 Class 文件的頭 4 個字節(jié)稱為魔數(shù)(magic),它的唯一作用是判斷該文件是否為一個能被虛擬機接受的 Class 文件。它的值固定為 0xCAFEBABE。緊接著 magic 的 4 個字節(jié)存儲的是 Class 文件的次版本號和主版本號,高版本的 JDK 能向下兼容低版本的 Class 文件,但不能運行更高版本的 Class 文件。
constant_pool
major_version 之后是常量池(constant_pool)的入口,它是 Class 文件中與其他項目關(guān)聯(lián)最多的數(shù)據(jù)類型,也是占用 Class 文件空間最大的數(shù)據(jù)項目之一。
常量池中主要存放兩大類常量:字面量和符號引用。字面量比較接近于 Java 層面的常量概念,如文本字符串、被聲明為 final 的常量值等。而符號引用總結(jié)起來則包括了下面三類常量:
虛擬機在加載 Class 文件時才會進行動態(tài)連接,也就是說,Class 文件中不會保存各個方法和字段的最終內(nèi)存布局信息,因此,這些字段和方法的符號引用不經(jīng)過轉(zhuǎn)換是無法直接被虛擬機使用的。當虛擬機運行時,需要從常量池中獲得對應(yīng)的符號引用,再在類加載過程中的解析階段將其替換為直接引用,并翻譯到具體的內(nèi)存地址中。
這里說明下符號引用和直接引用的區(qū)別與關(guān)聯(lián):
常量池中的每一項常量都是一個表,共有 11 種(JDK1.7 之前)結(jié)構(gòu)各不相同的表結(jié)構(gòu)數(shù)據(jù),沒中表開始的第一位是一個 u1 類型的標志位(1-12,缺少 2),代表當前這個常量屬于的常量類型。11 種常量類型所代表的具體含義如下表所示:
這 11 種常量類型各自均有自己的結(jié)構(gòu)。在 CONSTANT_Class_info 型常量的結(jié)構(gòu)中有一項 name_index 屬性,該常屬性中存放一個索引值,指向常量池中一個 CONSTANT_Utf8_info 類型的常量,該常量中即保存了該類的全限定名字符串。而 CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info 型常量的結(jié)構(gòu)中都有一項index屬性,存放該字段或方法所屬的類或接口的描述符 CONSTANT_Class_info 的索引項。另外,最終保存的諸如 Class 名、字段名、方法名、修飾符等字符串都是一個 CONSTANT_Utf8_info 類型的常量,也因此,Java 中方法和字段名的最大長度也即是CONSTANT_Utf8_info 型常量的最大長度,在 CONSTANT_Utf8_info 型常量的結(jié)構(gòu)中有一項 length 屬性,它是 u2 類型的,即占用 2 個字節(jié),那么它的最大的 length 即為 65535。因此,Java 程序中如果定義了超過 64KB 英文字符的變量或方法名,將會無法編譯。
下表給出了常量池中 11 種數(shù)據(jù)類型的結(jié)構(gòu):
常量 | 項目 | 類型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 值為1 |
length | u2 | UF-8編碼的字符串占用的字節(jié)數(shù) | |
bytes | u1 | 長度為length的UTF-8編碼的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值為3 |
bytes | u4 | 按照高位在前存儲的int值 | |
CONSTANT_Float_info | tag | u1 | 值為4 |
bytes | u4 | 按照高位在前存儲的float值 | |
CONSTANT_Long_info | tag | u1 | 值為5 |
bytes | u8 | 按照高位在前存儲的long值 | |
CONSTANT_Double_info | tag | u1 | 值為6 |
bytes | u8 | 按照高位在前存儲的double值 | |
CONSTANT_Class_info | tag | u1 | 值為7 |
index | u2 | 指向全限定名常量項的索引 | |
CONSTANT_String_info | tag | u1 | 值為8 |
index | u2 | 指向字符串字面量的索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值為9 |
index | u2 | 指向聲明字段的類或接口描述符CONSTANT_Class_info的索引項 | |
index | u2 | 指向字段名稱及類型描述符CONSTANT_NameAndType_info的索引項 | |
CONSTANT_Methodref_info | tag | u1 | 值為10 |
index | u2 | 指向聲明方法的類描述符CONSTANT_Class_info的索引項 | |
index | u2 | 指向方法名稱及類型描述符CONSTANT_NameAndType_info的索引項 | |
CONSTANT_InrerfaceMethodref_info | tag | u1 | 值為11 |
index | u2 | 指向聲明方法的接口描述符CONSTANT_Class_info的索引項 | |
index | u2 | 指向方法名稱及類型描述符CONSTANT_NameAndType_info的索引項 | |
CONSTANT_NameAndType_info | tag | u1 | 值為12 |
index | u2 | 指向字段或方法名稱常量項目的索引 | |
index | u2 | 指向該字段或方法描述符常量項的索引 |
access_flag
在常量池結(jié)束之后,緊接著的 2 個字節(jié)代表訪問標志(access_flag),這個標志用于識別一些類或接口層次的訪問信息,包括:這個 Class 是類還是接口,是否定義為 public 類型,abstract 類型,如果是類的話,是否聲明為 final,等等。每種訪問信息都由一個十六進制的標志值表示,如果同時具有多種訪問信息,則得到的標志值為這幾種訪問信息的標志值的邏輯或。
this_class、super_class、interfaces
類索引(this_class)和父類索引(super_class)都是一個 u2 類型的數(shù)據(jù),而接口索引集合(interfaces)則是一組 u2 類型的數(shù)據(jù)集合,Class 文件中由這三項數(shù)據(jù)來確定這個類的繼承關(guān)系。類索引、父類索引和接口索引集合都按照順序排列在訪問標志之后,類索引和父類索引兩個 u2 類型的索引值表示,它們各自指向一個類型為 COMNSTANT_Class_info 的類描述符常量,通過該常量中的索引值找到定義在 COMNSTANT_Utf8_info 類型的常量中的全限定名字符串。而接口索引集合就用來描述這個類實現(xiàn)了哪些接口,這些被實現(xiàn)的接口將按 implements 語句(如果這個類本身是個接口,則應(yīng)當是 extend 語句)后的接口順序從左到右排列在接口的索引集合中。
fields
字段表(field_info)用于描述接口或類中聲明的變量。字段包括了類級變量或?qū)嵗壸兞?,但不包括在方法?nèi)聲明的變量。字段的名字、數(shù)據(jù)類型、修飾符等都是無法固定的,只能引用常量池中的常量來描述。下面是字段表的最種格式:
其中的 access_flags 與類中的 access_flagsfei 類似,是表示數(shù)據(jù)類型的修飾符,如 public、static、volatile 等。后面的 name_index 和 descriptor_index 都是對常量池的引用,分別代表字段的簡單名稱及字段和方法的描述符。這里簡單解釋下“簡單名稱”、“描述符”和“全限定名”這三種特殊字符串的概念。
前面有所提及,全限定名即指一個事物的完整的名稱,如在 org.lxh.test 包下的 TestClass 類的全限定名為:org/lxh/test/TestClass
,即把包名中的“.”改為“/”,為了使連續(xù)的多個全限定名之間不產(chǎn)生混淆,在使用時最后一般會加入一個“,”來表示全限定名結(jié)束。簡單名稱則是指沒有類型或參數(shù)修飾的方法或字段名稱,如果一個類中有這樣一個方法 boolean get(int name)和一個變量 private final static int m,則他們的簡單名稱則分別為 get()和 m。
而描述符的作用則是用來描述字段的數(shù)據(jù)類型、方法的參數(shù)列表(包括數(shù)量、類型以及順序等)和返回值的。根據(jù)描述符規(guī)則,詳細的描述符標示字的含義如下表所示:
對于數(shù)組類型,每一維度將使用一個前置的 “[” 字符來描述,如一個整數(shù)數(shù)組 “int [][]” 將為記錄為 “[[I” ,而一個 String 類型的數(shù)組 “String[]” 將被記錄為 “[Ljava/lang/String” 。
用方法描述符描述方法時,按照先參數(shù)后返回值的順序描述,參數(shù)要按照嚴格的順序放在一組小括號內(nèi),如方法 int getIndex(String name,char[] tgc,int start,int end,char target) 的描述符為 “(Ljava/lang/String[CIIC)I”。
字段表包含的固定數(shù)據(jù)項目到 descriptor_index 為止就結(jié)束了,但是在它之后還緊跟著一個屬性表集合用于存儲一些額外的信息。比如,如果在類中有如下字段的聲明:staticfinalint m = 2;那就可能會存在一項名為ConstantValue 的屬性,它指向常量 2。關(guān)于 attribute_info 的詳細內(nèi)容,在后面關(guān)于屬性表的項目中會有詳細介紹。
最后需要注意一點:字段表集合中不會列出從父類或接口中繼承而來的字段,但有可能列出原本 Java 代碼中不存在的字段。比如在內(nèi)部類中為了保持對外部類的訪問性,會自動添加指向外部類實例的字段。
methods
方法表(method_info)的結(jié)構(gòu)與屬性表的結(jié)構(gòu)相同,不過多贅述。方法里的 Java 代碼,經(jīng)過編譯器編譯成字節(jié)碼指令后,存放在方法屬性表集合中一個名為“Code”的屬性里,關(guān)于屬性表的項目,同樣會在后面詳細介紹。
與字段表集合相對應(yīng),如果父類方法在子類中沒有被覆寫,方法表集合中就不會出現(xiàn)來自父類的方法信息。但同樣,有可能會出現(xiàn)由編譯器自動添加的方法,最典型的便是類構(gòu)造器 “” 方法和實例構(gòu)造器 “” 方法。
在 Java 語言中,要重載一個方法,除了要與原方法具有相同的簡單名稱外,還要求必須擁有一個與原方法不同的特征簽名,特征簽名就是一個方法中各個參數(shù)在常量池中的字段符號引用的集合,也就是因為返回值不會包含在特征簽名之中,因此 Java 語言里無法僅僅依靠返回值的不同來對一個已有方法進行重載。
ttributes
屬性表(attribute_info)在前面已經(jīng)出現(xiàn)過多系,在 Class 文件、字段表、方法表中都可以攜帶自己的屬性表集合,以用于描述某些場景專有的信息。
屬性表集合的限制沒有那么嚴格,不再要求各個屬性表具有嚴格的順序,并且只要不與已有的屬性名重復(fù),任何人實現(xiàn)的編譯器都可以向?qū)傩员碇袑懭胱约憾x的屬性信息,但 Java 虛擬機運行時會忽略掉它不認識的屬性。Java 虛擬機規(guī)范中預(yù)定義了 9 項虛擬機應(yīng)當能識別的屬性(JDK1.5 后又增加了一些新的特性,因此不止下面 9 項,但下面 9 項是最基本也是必要,出現(xiàn)頻率最高的),如下表所示:
對于每個屬性,它的名稱都需要從常量池中引用一個 CONSTANT_Utf8_info 類型的常量來表示,每個屬性值的結(jié)構(gòu)是完全可以自定義的,只需說明屬性值所占用的位數(shù)長度即可。一個符合規(guī)則的屬性表至少應(yīng)具有 “attribute_name_info”、“attribute_length” 和至少一項信息屬性。
Code 屬性
前面已經(jīng)說過,Java 程序方法體中的代碼講過 javac 編譯后,生成的字節(jié)碼指令便會存儲在 Code 屬性中,但并非所有的方法表都必須存在這個屬性,比如接口或抽象類中的方法就不存在 Code 屬性。如果方法表有 Code 屬性存在,那么它的結(jié)構(gòu)將如下表所示:
attribute_name_index 是一項指向 CONSTANT_Utf8_info 型常量的索引,常量值固定為 “Code”,它代表了該屬性的名稱。attribute_length 指示了屬性值的長度,由于屬性名稱索引與屬性長度一共是 6 個字節(jié),所以屬性值的長度固定為整個屬性表的長度減去 6 個字節(jié)。
max_stack 代表了操作數(shù)棧深度的最大值,max_locals 代表了局部變量表所需的存儲空間,它的單位是Slot,并不是在方法中用到了多少個局部變量,就把這些局部變量所占 Slot 之和作為 max_locals 的值,原因是局部變量表中的 Slot 可以重用。
code_length 和 code 用來存儲 Java 源程序編譯后生成的字節(jié)碼指令。code 用于存儲字節(jié)碼指令的一系列字節(jié)流,它是 u1 類型的單字節(jié),因此取值范圍為 0x00 到 0xFF,那么一共可以表達 256 條指令,目前,Java 虛擬機規(guī)范已經(jīng)定義了其中 200 條編碼值對應(yīng)的指令含義。code_length 雖然是一個 u4 類型的長度值,理論上可以達到 2^32-1,但是虛擬機規(guī)范中限制了一個方法不允許超過 65535 條字節(jié)碼指令,如果超過了這個限制,Javac 編譯器將會拒絕編譯。
字節(jié)碼指令之后是這個方法的顯式異常處理表集合(exception_table),它對于 Code 屬性來說并不是必須存在的。它的格式如下表所示:
它包含四個字段,這些字段的含義為:如果字節(jié)碼從第 start_pc 行到第 end_pc 行之間(不含 end_pc 行)出現(xiàn)了類型為 catch_type 或其子類的異常(catch_type為指向一個 CONSTANT_Class_info 型常量的索引),則轉(zhuǎn)到第 handler_pc 行繼續(xù)處理,當 catch_pc 的值為 0 時,代表人和的異常情況都要轉(zhuǎn)到 handler_pc 處進行處理。異常表實際上是 Java 代碼的一部分,編譯器使用異常表而不是簡單的跳轉(zhuǎn)命令來實現(xiàn) Java 異常即 finally 處理機制,也因此,finally 中的內(nèi)容會在 try 或 catch 中的 return 語句之前執(zhí)行,并且在 try 或 catch 跳轉(zhuǎn)到 finally 之前,會將其內(nèi)部需要返回的變量的值復(fù)制一份副本到最后一個本地表量表的 Slot中,也因此便有了http://blog.csdn.net/ns_code/article/details/17485221這篇文章中出現(xiàn)的情況。
Code 屬性是 Class 文件中最重要的一個屬性,如果把一個 Java 程序中的信息分為代碼和元數(shù)據(jù)兩部分,那么在整個 Class 文件里,Code 屬性用于描述代碼,所有的其他數(shù)據(jù)項目都用于描述元數(shù)據(jù)。
Exception 屬性
這里的 Exception 屬性的作用是列舉出方法中可能拋出的受查異常,也就是方法描述時在 throws 關(guān)鍵字后面列舉的異常。它的結(jié)構(gòu)很簡單,只有 attribute_name_index、attribute_length、number_of_exceptions、exception_index_table 四項,從字面上便很容易理解,這里不再詳述。
LineNumberTable 屬性
它用于描述 Java 源碼行號與字節(jié)碼行號之間的對應(yīng)關(guān)系。
LocalVariableTable 屬性
它用于描述棧幀中局部變量表中的變量與 Java 源碼中定義的變量之間的對應(yīng)關(guān)系。
SourceFile 屬性
它用于記錄生成這個 Class 文件的源碼文件名稱。
ConstantValue 屬性
ConstantValue 屬性的作用是通知虛擬機自動為靜態(tài)變量賦值,只有被 static 修飾的變量才可以使用這項屬性。在 Java 中,對非 static 類型的變量(也就是實例變量)的賦值是在實例構(gòu)造器方法中進行的;而對于類變量(static 變量),則有兩種方式可以選擇:在類構(gòu)造其中賦值,或使用 ConstantValue 屬性賦值。
目前 Sun Javac 編譯器的選擇是:如果同時使用 final 和 static 修飾一個變量(即全局常量),并且這個變量的數(shù)據(jù)類型是基本類型或 String 的話,就生成 ConstantValue 屬性來進行初始化(編譯時 Javac 將會為該常量生成 ConstantValue 屬性,在類加載的準備階段虛擬機便會根據(jù) ConstantValue 為常量設(shè)置相應(yīng)的值),如果該變量沒有被 final 修飾,或者并非基本類型及字符串,則選擇在方法中進行初始化。
雖然有 final 關(guān)鍵字才更符合”ConstantValue“的含義,但在虛擬機規(guī)范中并沒有強制要求字段必須用 final 修飾,只要求了字段必須用 static 修飾,對 final 關(guān)鍵字的要求是 Javac 編譯器自己加入的限制。因此,在實際的程序中,只有同時被 final 和 static 修飾的字段才有 ConstantValue 屬性。而且 ConstantValue 的屬性值只限于基本類型和 String,很明顯這是因為它從常量池中也只能夠引用到基本類型和 String 類型的字面量。
ConstantValue 屬性的作用是通知虛擬機自動為靜態(tài)變量賦值,只有被 static 修飾的變量才可以使用這項屬性。在 Java 中,對非 static 類型的變量(也就是實例變量)的賦值是在實例構(gòu)造器方法中進行的;而對于類變量(static 變量),則有兩種方式可以選擇:在類構(gòu)造其中賦值,或使用 ConstantValue 屬性賦值。
目前 Sun Javac 編譯器的選擇是:如果同時使用 final 和 static 修飾一個變量(即全局常量),并且這個變量的數(shù)據(jù)類型是基本類型或 String 的話,就生成 ConstantValue 屬性來進行初始化(編譯時 Javac 將會為該常量生成 ConstantValue 屬性,在類加載的準備階段虛擬機便會根據(jù) ConstantValue 為常量設(shè)置相應(yīng)的值),如果該變量沒有被 final 修飾,或者并非基本類型及字符串,則選擇在方法中進行初始化。
雖然有 final 關(guān)鍵字才更符合”ConstantValue“的含義,但在虛擬機規(guī)范中并沒有強制要求字段必須用 final 修飾,只要求了字段必須用 static 修飾,對 final 關(guān)鍵字的要求是 Javac 編譯器自己加入的限制。因此,在實際的程序中,只有同時被 final 和 static 修飾的字段才有 ConstantValue 屬性。而且 ConstantValue 的屬性值只限于基本類型和 String,很明顯這是因為它從常量池中也只能夠引用到基本類型和 String 類型的字面量。
下面簡要說明下 final、static、static final 修飾的字段賦值的區(qū)別:
InnerClasses 屬性
該屬性用于記錄內(nèi)部類與宿主類之間的關(guān)聯(lián)。如果一個類中定義了內(nèi)部類,那么編譯器將會為它及它所包含的內(nèi)部類生成 InnerClasses 屬性。
Deprecated 屬性和 Synthetic 屬性
該屬性用于表示某個類、字段和方法,已經(jīng)被程序作者定為不再推薦使用,它可以通過在代碼中使用 @Deprecated 注釋進行設(shè)置。
Synthetic 屬性
該屬性代表此字段或方法并不是 Java 源代碼直接生成的,而是由編譯器自行添加的,如 this 字段和實例構(gòu)造器、類構(gòu)造器等。
更多建議: