class文件是指以.class為文件後綴的Java虛擬機可裝載文件。無論該class文件是在linux上進行編譯的,還是在windows環境下編譯的,無論虛擬機是在何種平台下實現和運行的,class文件使得Java虛擬機可以正確的讀取、解釋所有的class文件。 在分析和研究class文件之前,先提出有一些問題:
1.類/接口(class文件也可能定義的是接口,所以還是不要理解為類文件為好)內有哪些內容?
2.以上內容分別保存在class文件的什麼地方?
3.這些內容在加載過程中又如何被讀取和解析?
4.這些內容加載後又會被解析成為什麼樣的數據結構保存在虛擬機中?
5.這些數據結構在虛擬機的運行過程中又是如何被使用的?
擴展問題:
6.如何防止class文件被劫持?
7.如何防止class文件被反編譯?
class文件的組織結構定義如下:
ClassFile{
magic u4,
minor_version u2,
major_version u2,
constant_pool_count u2,
constant_pool cp_info*constant_pool_count,
access_flags u2,
this_class u2,
super_class u2,
interface_count u2,
interfaces u2 * interface_count,
fields_count u2,
fields field_info * fields_count,
methods_count u2,
methods method_info * methods_count,
attributes_count u2,
attributes attributes_info * attributes_count
}
以如下程序為例,對生成的class文件進行分析:
//TestInterface.java
public interface TestInterface {
public void interface_method();
}
//TestClass.java
public class TestClass implements TestInterface{
private int private_global = 3;
public int public_global;
private static final int sfi = 127;
public static final String sfs = "test strings";
private StringBuilder sb;
public void method_1(){
private_global = public_global * 2;
sb.append(private_global);
}
public void method_2(int pub){
public_global = pub;
}
public void method_2(int pub, boolean flag){
int tmp = 5;
public_global = pub * 2 + tmp;
}
@Override
public void interface_method() {
method_1();
}
}
1.magic(魔數) 值為0xcafebabe,沒有特別的意義,放在文件頭並選取用來標記改文件是一個class文件。
2.minor_version/major_version(次版本號和主版本號)
次版本號和主版本號分別為0x0000和0x0032(50),即主版本號位50,次版本號為0
3.constant_pool_count/constant_pool(常量池數量和常量池)
常量池保存了文件中類或接口相關的一切常量,字面常量(直接量),如文字字符串、final變量值,以及符號引用,如類或接口的全限定名、方法或字段的簡單名稱和描述符。
其中,全限定名用以在當前命名空間內唯一標志類或接口,在java語言中如java.lang.Object,在class文件中,會將'.'用'/'取代,即表示為java/lang/Object 簡單名稱就是簡單的方法名或變量名的字符串,如java.lang.Object的成員方法wait()的簡單名稱為"wait"。
而只有簡單名稱是無法唯一確定調用的方法是哪一個,由於Java語言的特性,方法可能被重寫或重載, 所以還需要根據方法的返回值、參數數量、類型、順序來確定一個方法描述符來唯一標志該方法,字段的描述符則簡單得多,只需要給出字段的類型 描述符讓我們聯想起PE/ELF文件的函數簽名,它由上下文無關語法定義:
FieldDescriptor:
FieldType
ComponentType:
FieldType
FieldType:
BaseType
ObjectType
ArrayType
BaseType:
B
C
D
F
I
J
S
Z
ObjectType:
L<classname>;
ArrayType:
[ComponentType
MethodDescriptor:
(ParameterDescriptor*) ReturnDescriptor
ParameterDescriptor:
FieldType
ReturnDescriptor:
FieldType
V
其終結符號如下:
以深入java虛擬機上的示例作為參考:
下面看class文件內常量池部分: 首先是常量池數:即(0x35)53個常量池
Java虛擬機將常量池組織成為列表(可以看做是一個常量池的數組)的形式,常量池內容可能指向其他常量池,並且class文件中其他部分內容也可能指向常量池入口,這些常量池通過該常量池在常量池列表中的索引來定位,常量池列表的0號常量池其實是空的,作為常量池的NULL引用,即常量池列表的第一項實際上是1號常量池,常量池列表實際上只有constant_pool_count - 1個常量池項。 隨後是常量池列表,常量池的結構如下:
cp_info{ tag, info }
常量池的固定第一個字節是常量值標簽,用來描述該常量池保存內容的類型,常量池標志和含義如下:
根據常量池標志tag的不同,info有不同的組織方式:
(1).CONSTANT_Utf8結構:
(可以看出length由2個字節表示,最大長度就應該是65536字節)
該類型是一個長度可變(長度為length)的常量字符串表,用來存儲以下類型的字符串:
字符的存放:
對於0x0001-0x007f的字符將使用一個字節(該字節的0-6位,第7位為0)存放
對於0x080-0x07ff的字符將使用兩個字節(依次高字節的0-5位和低字節的0-4位,剩余位分別為10、110)存放
對於0x0800-0xffff的字符將使用3個字節(依次為高字節的0-5中間字節的0-5,和低字節的0-3位,剩余位分別為10、10、1110)存放。
(2).CONSTANT_Integer結構:
按高位在前的格式存儲int型數據
(3).CONSTANT_Float結構:
按高位在前的格式存儲float型數據
(4).CONSTANT_Doube結構:
按高位在前的格式存儲double型數據
(5).CONSTANT_Long結構:
按高位在前的格式存儲long型數據
(6).CONSTANT_Class結構:
name_index為類或者接口符號引用的CONSTANT_Utf8常量池的索引(全限定名)
(7).CONSTANT_String結構:
string_index為字符串的CONSTANT_Utf8常量池的索引
(8).CONSTANT_Fieldref結構:
描述了指向字段的符號引用,其內容分兩項表示,一項為被引用字段所在類或接口的CONSTANT_Class常量池索引,一項為字段的簡單名稱和描述符,指向一個CONSTANT_NameAndType常量池
(9).CONSTANT_Methodref結構:
與CONSTANT_Fieldref類似,描述了指向類中聲明的方法的符號引用,其內容分兩項表示,一項為被引用方法所在類的CONSTANT_Class常量池索引,一項為方法的簡單名稱和描述符,指向一個CONSTANT_NameAndType常量池
(10).CONSTANT_InterfaceMethodref結構:
與CONSTANT_Methodref類似,描述了指向接口中聲明的方法的符號引用,其內容分兩項表示,一項為被引用方法所在接口的CONSTANT_Class常量池索引,一項為方法的簡單名稱和描述符,指向一個CONSTANT_NameAndType常量池
(11).CONSTANT_NameAndType結構:
可以預見,該常量池提供了所引用字段或方法的簡單名稱和常量池入口
注意區分class_index指向的是對應類的常量池,該CONSTANT_Class常量池指向一個全限定名的CONSTANT_Utf8字符串常量池
常量池部分的解析可以參考http://note.youdao.com/share/?id=3c1f3fac45837f95cc87fa6694a25b84&type=note
4.access_flags
該項2字節標志了所定義類或接口的類型信息
該文件中access_flags為0x0021 ,可見該類是public super類型。
5.this_class(當前類)
該項2字節標志了所定義類或接口的CONSTANT_Class常量池索引,最終指向全限定名”TestClass”
6.super_class(超類)
該項2字節標志了所定義類的超類的CONSTANT_Class常量池索引,最終指向全限定名”java/lang/Object”
7.interfaces_count/interfaces(接口數和接口)
首先2字節是在該類中直接實現或擴展的接口數,後面緊隨若干個(接口數)2字節,代表所直接實現或擴展的接口的CONSTANT_Class常量池的索引
這裡只實現了一個接口,就是5號常量池,即全限定名”TestInterface”所定義的接口
8.fields_count/fields(字段數和字段)
fields_count是類變量(靜態變量)和實例變量(非靜態變量)的字段數總和,與constant_pool組織形式類似,後面是fields_count個field_info,需要注意的是,當前類的字段不會包含其超類或父接口中繼承的字段,也會包含在Java源文件中沒有但是在編譯時添加的一些字段。field_info結構如下:
field_info{ access_flags u2, name_index u2, descriptor_index u2, attributes_count u2, attributes attributes_info * attributes_cout }
(1).字段的accesss_flags與描述當前類的access_flags不同:
類中聲明的字段,只能擁有ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三個標志中的一個。ACC_FINAL
和ACC_VOLATILE 不能同時設置。所有接口中聲明的字段必須有ACC_PUBLIC、ACC_STATIC、ACC_FINAL 這三種標志。
(2).name_index為該字段的簡單名稱的CONSTANT_Utf8常量池索引
(3).descriptor_index為該字段的描述符的CONSTANT_Utf8常量池索引
(4).attributes_count和attributes是attributes_count個attribute_info結構所表述的屬性集合。在字段域出現的屬性有ConstantValue(final常量)、Deprecated(被禁用的指示符)、Synthetic(編譯器產生的指示符)
屬性出現在ClassFile、field_info、method_info、Code_attribute中。所有Java虛擬機必須能夠識別Code、ConstantValue、Exception。對於能夠正常實現Java/Java2平台類庫的虛擬機必須能夠識別InnerClass和Synthetic屬性。
attribute_info的結構如下:
attribute_info{ attribute_name_index u2, attribute_length u4, info u1, }
attribute_name_index為描述屬性的字符串名稱(即上述列出屬性名)的CONSTANT_Utf8常量池索引,
attribute_length為後面屬性內容的長度
這裡先介紹將字段可能用到的ConstantValue、Deprecated和Synthetic屬性
(1).ConstantValue
該屬性用於描述值為常量的字段,並且在包含該屬性的字段其access_flag必須為ACC_STATIC,以表明這是一個靜態常量。
constantvalue_index指向提供常量值的常量池索引(此外,ConstantValue對應的屬性的attribute_length始終為2)
(2).Deprecated
被@Deprecated所注釋的字段、方法或類型,表示雖然該字段、方法或類型仍然存在,但是不建議使用,其在未來的版本中可能會被移除
Deprecated對應的屬性的attribute_length值始終為0
(3).Synthetic
用來指明為編譯器所產生的字段、方法或類型
同樣,這是一個固定長度屬性,其
對應的屬性的attribute_length值始終為0
class文件field域解析:
首先由開頭兩個字節看出有5個field_info
field1:
access_flag為ACC_PRIVATE,標志其為private類型
name_index為0x0007,指向7號常量池,即簡單名稱為”private_global”
descriptor_index為0x0008,指向8號常量池,即描述符為”I”
attributes_count為0,即沒有任何屬性
field2:
access_flag為ACC_PUBLIC,標志其為public類型
name_index為0x0009,指向9號常量池,即簡單名稱為”public_global”
descriptor_index為0x0008,指向8號常量池,即描述符為”I”
attributes_count為0,即沒有任何屬性
field3:
access_flag為0x0010|0x0008|0x0002,即ACC_FINAL | ACC_STATIC | ACC_PRIVATE,標志其為private static final類型
name_index為0x000A,指向10號常量池,即簡單名稱為”sfi”
descriptor_index為0x0008,指向8號常量池,即描述符為”I”
attributes_count為1,即有一個屬性
該屬性的
attribute_name_index為0x000B,指向11號常量池,即”ConstantValue”屬性
attribute_length為2,即固定2個字節
constantvalue_index為0x000C,指向12號常量池,即sfi的值為”127”(這裡還是字符串)
field4:
access_flag為0x0010|0x0008|0x0001,即ACC_FINAL | ACC_STATIC | ACC_PUBLIC,標志其為public static final類型
name_index為0x000D,指向13號常量池,即簡單名稱為”sfs”
descriptor_index為0x000E,指向14號常量池,即描述符為”Ljava/lang/String;”
attributes_count為1,即有一個屬性
該屬性的
attribute_name_index為0x000B,指向11號常量池,即”ConstantValue”屬性
attribute_length為2,即固定2個字節
constantvalue_index為0x000F,指向15號常量池,即sfs的值為”test strings”
field5:
access_flag為ACC_PRIVATE,標志其為private類型
name_index為0x0011,指向17號常量池,即簡單名稱為”sb”
descriptor_index為0x0012,指向18號常量池,即描述符為”Ljava/lang/StringBuilder;”
attributes_count為0,即沒有任何屬性
9.methods_count/methods(方法數/方法)
方法域的method_info結構與字段域是一樣的,即
method_info{ access_flags u2, name_index u2, descriptor_index u2, attributes_count u2, attributes attributes_info * attributes_cout }
不過其access_flag有些不同
如果一個方法是抽象方法,那麼它就不能為private、static、final、synchronized、native和strict類型
在方法域出現的屬性有Code、Deprecated、Exceptions、Synthetic
下面介紹新出現的兩種屬性Code和Exceptions:
(1).Code
其info域的結構如下
其中:
首先看exception_table_info的結構,可以預見,一個異常在代碼中的描述就必須包含作用域、異常類型和異常處理三部分內容,看看exception_table_info是不是這樣組織的
exception_table_info{ start_pc u2, end_pc u2, handler_pc u2, catch_type u2, }
不出所料,start_pc就是異常處理器起始位置相對該段代碼的偏移量,
end_pc就是異常處理器結束位置相對該段代碼的偏移量,
handler_pc就是異常處理器第一條指令相對該段代碼的偏移量
catch_type指向描述該異常類型(java/lang/Throwable或其子類)的CONSTANT_Class常量池索引,二若catch_type為0,那麼異常處理器將處理所有異常
(2).LineNumberTable
行號表與ELF/PE文件看上去有著異曲同工之妙,它同樣建立了方法的字節碼偏移量和源代碼行號之間的映射關系。其info域結構如下
line_number_table_length描述了行號表的項數,注意,並不是行號表各項並不是逐行對應,而是可能按照任何順序排列,並且可能多項對應同一行。
line_number_info的結構如下:
line_number_info{ start_pc u2, line_number u2, }
其中,start_pc描述了該行起始第一個字節碼對應該段代碼的偏移量,line_number描述了對應的行號。
(3).LocalVariableTable
這裡由LocalVariableTable保存了方法的棧幀中局部變量域源代碼中局部變量的名稱和描述符之間的映射關系。
同樣,局部變量表也是以local_variable_table_length個local_variable_info結構進行組織的
local_variable_info的結構如下:
local_variable_info{ start_pc u2, length u2, name_index u2, descriptor_index u2, index u2, }
(4).Exceptions屬性
區別於描述Code屬性的exception_table部分,這裡是方法可能會拋出的異常,而非包圍代碼的try/catch異常。Exceptions屬性的info域格式如下:
exception_index_table是該方法拋出的異常類型的CONSTANT_Class常量池索引,number_of_exceptions指出了拋出異常類型的數量。
methods部分的解析可以參考http://note.youdao.com/share/?id=b1c762ba1ee4874a23eb8a512cccf507&type=note
10.attributes_count/attributes(屬性數和屬性)
最後還有兩種屬性:InnerClass和SourceFile
(1).SourceFile
其info結構為:
給出了指向源文件名的CONSTANT_Utf8常量池索引
如該class文件最後的attributes_count為1,其
attribute_name_index為0x0033,指向51號常量池,即”SourceFile”屬性
attribute_length為0x02,即2個字節
sourcefile_index為0x0034,指向52號常量池,即源文件名為”TestClass.java”
(2).InnerClasses
classses_info描述了內部類(成員嵌套類、局部嵌套類和匿名嵌套類)的信息,其結構如下:
classes_info{ inner_class_info_index u2, outer_class_info_index u2, inner_name_index u2, inner_class_access_flags u2, }
以如下內容為例
其生成的class文件如下:
其InnerClasses屬性內容為:
其中匿名內部類Runnable的全限定名為InnerClassTest$1,由於其不是一個成員嵌套類(該類是局部嵌套類),其outer_class_info_index 為0,由於該類是一個匿名內部類,其inner_name_index為0(即簡單名稱為空)
局部嵌套類NestedLocalClass的全限定名為InnerClassTest$1NestedLocalClass,由於其不是一個成員嵌套類,其outer_class_info_index為0,其簡單名稱為”NestedLocalClass”,access_flag為final
成員嵌套類NestedMemberClass的全限定名為InnerClassTest$NestedMemberClass,其簡單名稱為”NestedMemberClass”,access_flag為public static final
此外,我們注意到內嵌類的內容會定義在各自的class文件中,而不會出現在InnerClassTest類的class文件中,在NestedMemberClass的class文件中有著如下的InnerClasses屬性:
在subClass的class文件中也有著如下的InnerClasses屬性:
可以看出,每個作為外圍類的內部類的類都將保存在該外圍類的CONSTANT_Class常量池中,並有一個inner_class_info結構加以描述
如InnerClassTest的3個內部類項,NestedMemberClass的第二個內部類項
但是需要注意,subClass在被沒有被InnerClassTest直接引用時,是不會出現在InnerClassTest的InnerClasses屬性中的
另外,InnerClasses還將表述內嵌類型的外圍類,作為內部類的所有外圍類都將保存在該內部類的CONSTANT_Class常量池中,並有一個inner_class_info結構加以描述
如NestedMemberClass的第1個外部類項,subClass的2個外部類項