轉載請注明出處:http://blog.csdn.net/ns_code/article/details/17675609
平台無關性
Java是與平台無關的語言,這得益于Java源代碼編譯後生成的存儲位元組碼的檔案,即Class檔案,以及Java虛拟機的實作。不僅使用Java編譯器可以把Java代碼編譯成存儲位元組碼的Class檔案,使用JRuby等其他語言的編譯器也可以把程式代碼編譯成Class檔案,虛拟機并不關心Class的來源是什麼語言,隻要它符合一定的結構,就可以在Java中運作。Java語言中的各種變量、關鍵字和運算符的語義最終都是由多條位元組碼指令組合而成的,是以位元組碼指令所能提供的語義描述能力肯定會比Java語言本身更強大,這便為其他語言實作一些有别于Java的語言特性提供了基礎,而且這也正是在類加載時要進行安全驗證的原因。
類檔案結構
Class檔案是一組以8位位元組為基礎機關的二進制流,各個資料項目嚴格按照順序緊湊地排列在Class檔案中,中間沒有添加任何分隔符,這使得整個Class檔案中存儲的内容幾乎全部都是程式運作的必要資料。根據Java虛拟機規範的規定,Class檔案格式采用一種類似于C語言結構體的僞結構來存儲,這種僞結構中隻有兩種資料類型:無符号數和表。無符号數屬于基本資料類型,以u1、u2、u4、u8來分别代表1、2、4、8個位元組的無符号數。表是由多個無符号數或其他表作為資料項構成的符合資料類型,所有的表都習慣性地以“_info”結尾。
整個Class檔案本質上就是一張表,它由如下所示的資料項構成。
從表中可以看出,無論是無符号數還是表,當需要描述同一類型但數量不定的多個資料時,經常會使用一個前置的容量計數器加若幹個連續的該資料項的形式,稱這一系列連續的摸一個類型的資料為某一類型的集合,比如,fields_count個field_info表資料構成了字段表集合。這裡需要說明的是:Class檔案中的資料項,都是嚴格按照上表中的順序和數量被嚴格限定的,每個位元組代表的含義,長度,先後順序等都不允許改變。
下表列出了Class檔案中各個資料項的具體含義:

從表中可以看出,無論是無符号數還是表,當需要描述同一類型但數量不定的多個資料時,經常會在其前面使用一個前置的容量計數器來記錄其數量,而便跟着若幹個連續的資料項,稱這一系列連續的某一類型的資料為某一類型的集合,如:fields_count個field_info表資料便組成了方法表集合。這裡需要注意的是:Class檔案中各資料項是按照上表的順序和數量被嚴格限定的,每個位元組代表的含義、長度、先後順序都不允許改變。
magic與version
每個Class檔案的頭4個位元組稱為魔數(magic),它的唯一作用是判斷該檔案是否為一個能被虛拟機接受的Class檔案。它的值固定為0xCAFEBABE。緊接着magic的4個位元組存儲的是Class檔案的次版本号和主版本号,高版本的JDK能向下相容低版本的Class檔案,但不能運作更高版本的Class檔案。
constant_pool
major_version之後是常量池(constant_pool)的入口,它是Class檔案中與其他項目關聯最多的資料類型,也是占用Class檔案空間最大的資料項目之一。
常量池中主要存放兩大類常量:字面量和符号引用。字面量比較接近于Java層面的常量概念,如文本字元串、被聲明為final的常量值等。而符号引用總結起來則包括了下面三類常量:
- 類和接口的全限定名(即帶有包名的Class名,如:org.lxh.test.TestClass)
- 字段的名稱和描述符(private、static等描述符)
- 方法的名稱和描述符(private、static等描述符)
虛拟機在加載Class檔案時才會進行動态連接配接,也就是說,Class檔案中不會儲存各個方法和字段的最終記憶體布局資訊,是以,這些字段和方法的符号引用不經過轉換是無法直接被虛拟機使用的。當虛拟機運作時,需要從常量池中獲得對應的符号引用,再在類加載過程中的解析階段将其替換為直接引用,并翻譯到具體的記憶體位址中。
這裡說明下符号引用和直接引用的差別與關聯:
- 符号引用:符号引用以一組符号來描述所引用的目标,符号可以是任何形式的字面量,隻要使用時能無歧義地定位到目标即可。符号引用與虛拟機實作的記憶體布局無關,引用的目标并不一定已經加載到了記憶體中。
- 直接引用:直接引用可以是直接指向目标的指針、相對偏移量或是一個能間接定位到目标的句柄。直接引用是與虛拟機實作的記憶體布局相關的,同一個符号引用在不同虛拟機執行個體上翻譯出來的直接引用一般不會相同。如果有了直接引用,那說明引用的目标必定已經存在于記憶體之中了。
常量池中的每一項常量都是一個表,共有11種(JDK1.7之前)結構各不相同的表結構資料,沒中表開始的第一位是一個u1類型的标志位(1-12,缺少2),代表目前這個常量屬于的常量類型。11種常量類型所代表的具體含義如下表所示:
這11種常量類型各自均有自己的結構。在CONSTANT_Class_info型常量的結構中有一項name_index屬性,該常屬性中存放一個索引值,指向常量池中一個CONSTANT_Utf8_info類型的常量,該常量中即儲存了該類的全限定名字元串。而CONSTANT_Fieldref_info、CONSTANT_Methodref_info、CONSTANT_InterfaceMethodref_info型常量的結構中都有一項index屬性,存放該字段或方法所屬的類或接口的描述符CONSTANT_Class_info的索引項。另外,最終儲存的諸如Class名、字段名、方法名、修飾符等字元串都是一個CONSTANT_Utf8_info類型的常量,也是以,Java中方法和字段名的最大長度也即是CONSTANT_Utf8_info型常量的最大長度,在CONSTANT_Utf8_info型常量的結構中有一項length屬性,它是u2類型的,即占用2個位元組,那麼它的最大的length即為65535。是以,Java程式中如果定義了超過64KB英文字元的變量或方法名,将會無法編譯。
下表給出了常量池中11種資料類型的結構:
常量 | 項目 | 類型 | 描述 |
CONSTANT_Utf8_info | tag | u1 | 值為1 |
length | u2 | UF-8編碼的字元串占用的位元組數 | |
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
在常量池結束之後,緊接着的2個位元組代表通路标志(access_flag),這個标志用于識别一些類或接口層次的通路資訊,包括:這個Class是類還是接口,是否定義為public類型,abstract類型,如果是類的話,是否聲明為final,等等。每種通路資訊都由一個十六進制的标志值表示,如果同時具有多種通路資訊,則得到的标志值為這幾種通路資訊的标志值的邏輯或。
this_class、super_class、interfaces
類索引(this_class)和父類索引(super_class)都是一個u2類型的資料,而接口索引集合(interfaces)則是一組u2類型的資料集合,Class檔案中由這三項資料來确定這個類的繼承關系。類索引、父類索引和接口索引集合都按照順序排列在通路标志之後,類索引和父類索引兩個u2類型的索引值表示,它們各自指向一個類型為COMNSTANT_Class_info的類描述符常量,通過該常量中的索引值找到定義在COMNSTANT_Utf8_info類型的常量中的全限定名字元串。而接口索引集合就用來描述這個類實作了哪些接口,這些被實作的接口将按implements語句(如果這個類本身是個接口,則應當是extend語句)後的接口順序從左到右排列在接口的索引集合中。
fields
字段表(field_info)用于描述接口或類中聲明的變量。字段包括了類級變量或執行個體級變量,但不包括在方法内聲明的變量。字段的名字、資料類型、修飾符等都是無法固定的,隻能引用常量池中的常量來描述。下面是字段表的最種格式:
其中的access_flags與類中的access_flagsfei類似,是表示資料類型的修飾符,如public、static、volatile等。後面的name_index和descriptor_index都是對常量池的引用,分别代表字段的簡單名稱及字段和方法的描述符。這裡簡單解釋下“簡單名稱”、“描述符”和“全限定名”這三種特殊字元串的概念。
前面有所提及,全限定名即指一個事物的完整的名稱,如在org.lxh.test包下的TestClass類的全限定名為:org/lxh/test/TestClass,即把包名中的“.”改為“/”,為了使連續的多個全限定名之間不産生混淆,在使用時最後一般會加入一個“,”來表示全限定名結束。簡單名稱則是指沒有類型或參數修飾的方法或字段名稱,如果一個類中有這樣一個方法boolean get(int name)和一個變量private final static int m,則他們的簡單名稱則分别為get()和m。
而描述符的作用則是用來描述字段的資料類型、方法的參數清單(包括數量、類型以及順序等)和傳回值的。根據描述符規則,詳細的描述符标示字的含義如下表所示:
對于數組類型,每一次元将使用一個前置的“[”字元來描述,如一個整數數組“int [][]”将為記錄為“[[I”,而一個String類型的數組“String[]”将被記錄為“[Ljava/lang/String”
用方法描述符描述方法時,按照先參數後傳回值的順序描述,參數要按照嚴格的順序放在一組小括号内,如方法 int getIndex(String name,char[] tgc,int start,int end,char target)的描述符為“(Ljava/lang/String[CIIC)I”。
字段表包含的固定資料項目到descriptor_index為止就結束了,但是在它之後還緊跟着一個屬性表集合用于存儲一些額外的資訊。比如,如果在類中有如下字段的聲明:staticfinalint m = 2;那就可能會存在一項名為ConstantValue的屬性,它指向常量2。關于attribute_info的詳細内容,在後面關于屬性表的項目中會有詳細介紹。
最後需要注意一點:字段表集合中不會列出從父類或接口中繼承而來的字段,但有可能列出原本Java代碼中不存在的字段。比如在内部類中為了保持對外部類的通路性,會自動添加指向外部類執行個體的字段。
methods
方法表(method_info)的結構與屬性表的結構相同,不過多贅述。方法裡的Java代碼,經過編譯器編譯成位元組碼指令後,存放在方法屬性表集合中一個名為“Code”的屬性裡,關于屬性表的項目,同樣會在後面詳細介紹。
與字段表集合相對應,如果父類方法在子類中沒有被覆寫,方法表集合中就不會出現來自父類的方法資訊。但同樣,有可能會出現由編譯器自動添加的方法,最典型的便是類構造器“<clinit>”方法和執行個體構造器“<init>”方法。
在Java語言中,要重載一個方法,除了要與原方法具有相同的簡單名稱外,還要求必須擁有一個與原方法不同的特征簽名,特征簽名就是一個方法中各個參數在常量池中的字段符号引用的集合,也就是因為傳回值不會包含在特征簽名之中,是以Java語言裡無法僅僅依靠傳回值的不同來對一個已有方法進行重載。
attributes
屬性表(attribute_info)在前面已經出現過多系,在Class檔案、字段表、方法表中都可以攜帶自己的屬性表集合,以用于描述某些場景專有的資訊。
屬性表集合的限制沒有那麼嚴格,不再要求各個屬性表具有嚴格的順序,并且隻要不與已有的屬性名重複,任何人實作的編譯器都可以向屬性表中寫入自己定義的屬性資訊,但Java虛拟機運作時會忽略掉它不認識的屬性。Java虛拟機規範中預定義了9項虛拟機應當能識别的屬性(JDK1.5後又增加了一些新的特性,是以不止下面9項,但下面9項是最基本也是必要,出現頻率最高的),如下表所示:
對于每個屬性,它的名稱都需要從常量池中引用一個CONSTANT_Utf8_info類型的常量來表示,每個屬性值的結構是完全可以自定義的,隻需說明屬性值所占用的位數長度即可。一個符合規則的屬性表至少應具有“attribute_name_info”、“attribute_length”和至少一項資訊屬性。
1)Code屬性
前面已經說過,Java程式方法體中的代碼講過Javac編譯後,生成的位元組碼指令便會存儲在Code屬性中,但并非所有的方法表都必須存在這個屬性,比如接口或抽象類中的方法就不存在Code屬性。如果方法表有Code屬性存在,那麼它的結構将如下表所示:
attribute_name_index是一項指向CONSTANT_Utf8_info型常量的索引,常量值固定為“Code”,它代表了該屬性的名稱。attribute_length訓示了屬性值的長度,由于屬性名稱索引與屬性長度一共是6個位元組,是以屬性值的長度固定為整個屬性表的長度減去6個位元組。
max_stack代表了操作數棧深度的最大值,max_locals代表了局部變量表所需的存儲空間,它的機關是Slot,并不是在方法中用到了多少個局部變量,就把這些局部變量所占Slot之和作為max_locals的值,原因是局部變量表中的Slot可以重用。
code_length和code用來存儲Java源程式編譯後生成的位元組碼指令。code用于存儲位元組碼指令的一系列位元組流,它是u1類型的單位元組,是以取值範圍為0x00到0xFF,那麼一共可以表達256條指令,目前,Java虛拟機規範已經定義了其中200條編碼值對應的指令含義。code_length雖然是一個u4類型的長度值,理論上可以達到2^32-1,但是虛拟機規範中限制了一個方法不允許超過65535條位元組碼指令,如果超過了這個限制,Javac編譯器将會拒絕編譯。
位元組碼指令之後是這個方法的顯式異常處理表集合(exception_table),它對于Code屬性來說并不是必須存在的。它的格式如下表所示:
它包含四個字段,這些字段的含義為:如果位元組碼從第start_pc行到第end_pc行之間(不含end_pc行)出現了類型為catch_type或其子類的異常(catch_type為指向一個CONSTANT_Class_info型常量的索引),則轉到第handler_pc行繼續處理,當catch_pc的值為0時,代表人和的異常情況都要轉到handler_pc處進行處理。異常表實際上是Java代碼的一部分,編譯器使用異常表而不是簡單的跳轉指令來實作Java異常即finally處理機制,也是以,finally中的内容會在try或catch中的return語句之前執行,并且在try或catch跳轉到finally之前,會将其内部需要傳回的變量的值複制一份副本到最後一個本地表量表的Slot中,也是以便有了http://blog.csdn.net/ns_code/article/details/17485221這篇文章中出現的情況。
Code屬性是Class檔案中最重要的一個屬性,如果把一個Java程式中的資訊分為代碼和中繼資料兩部分,那麼在整個Class檔案裡,Code屬性用于描述代碼,所有的其他資料項目都用于描述中繼資料。
2)Exception屬性
這裡的Exception屬性的作用是列舉出方法中可能抛出的受查異常,也就是方法描述時在throws關鍵字後面列舉的異常。它的結構很簡單,隻有attribute_name_index、attribute_length、number_of_exceptions、exception_index_table四項,從字面上便很容易了解,這裡不再詳述。
3)LineNumberTable屬性
它用于描述Java源碼行号與位元組碼行号之間的對應關系。
4)LocalVariableTable屬性
它用于描述棧幀中局部變量表中的變量與Java源碼中定義的變量之間的對應關系。
5)SourceFile屬性
它用于記錄生成這個Class檔案的源碼檔案名稱。
6)ConstantValue屬性
ConstantValue屬性的作用是通知虛拟機自動為靜态變量指派,隻有被static修飾的變量才可以使用這項屬性。在Java中,對非static類型的變量(也就是執行個體變量)的指派是在執行個體構造器<init>方法中進行的;而對于類變量(static變量),則有兩種方式可以選擇:在類構造其中指派,或使用ConstantValue屬性指派。
目前Sun Javac編譯器的選擇是:如果同時使用final和static修飾一個變量(即全局常量),并且這個變量的資料類型是基本類型或String的話,就生成ConstantValue屬性來進行初始化(編譯時Javac将會為該常量生成ConstantValue屬性,在類加載的準備階段虛拟機便會根據ConstantValue為常量設定相應的值),如果該變量沒有被final修飾,或者并非基本類型及字元串,則選擇在<clinit>方法中進行初始化。
雖然有final關鍵字才更符合”ConstantValue“的含義,但在虛拟機規範中并沒有強制要求字段必須用final修飾,隻要求了字段必須用static修飾,對final關鍵字的要求是Javac編譯器自己加入的限制。是以,在實際的程式中,隻有同時被final和static修飾的字段才有ConstantValue屬性。而且ConstantValue的屬性值隻限于基本類型和String,很明顯這是因為它從常量池中也隻能夠引用到基本類型和String類型的字面量。
下面簡要說明下final、static、static final修飾的字段指派的差別:
- static修飾的字段在類加載過程中的準備階段被初始化為0或null等預設值,而後在初始化階段(觸發類構造器<clinit>)才會被賦予代碼中設定的值,如果沒有設定值,那麼它的值就為預設值。
- final修飾的字段在運作時被初始化(可以直接指派,也可以在執行個體構造器中指派),一旦指派便不可更改;
- static final修飾的字段在Javac時生成ConstantValue屬性,在類加載的準備階段根據ConstantValue的值為該字段指派,它沒有預設值,必須顯式地指派,否則Javac時會報錯。可以了解為在編譯期即把結果放入了常量池中。
7)InnerClasses屬性
該屬性用于記錄内部類與宿主類之間的關聯。如果一個類中定義了内部類,那麼編譯器将會為它及它所包含的内部類生成InnerClasses屬性。
8)Deprecated屬性和Synthetic屬性
該屬性用于表示某個類、字段和方法,已經被程式作者定為不再推薦使用,它可以通過在代碼中使用@Deprecated注釋進行設定。
9)Synthetic屬性
該屬性代表此字段或方法并不是Java源代碼直接生成的,而是由編譯器自行添加的,如this字段和執行個體構造器、類構造器等。