一、Class類文件結構
Class類文件嚴格按照順序緊湊的排列,由無符號數和表構成,表是由多個無符號數或其他數據項構成的符合數據結構。
Class類文件格式按如下順序排列:
| 類型 | 名稱 | 數量 |
| u4 | magic(魔術) | 1 |
| u2 | minor_version(次版本號) | 1 |
| u2 | major_version(主版本號) | 1 |
| u2 | constant_pool_count(常量個數) | 1 |
| cp_info | constant_pool(常量池表) | constant_pool_count-1 |
| u2 | access_flags(類的訪問控制權限) | 1 |
| u2 | this_class(類名) | 1 |
| u2 | super_class(父類名) | 1 |
| u2 | interfaces_count(接口個數) | 1 |
| u2 | interfaces(接口名) | interfaces_count |
| u2 | fields_count(域個數) | 1 |
| field_info | fields(域的表) | fields_count |
| u2 | methods_count(方法的個數) | 1 |
| method_info | methods(方法表) | methods_count |
| u2 | attributes_count(附加屬性的個數) | 1 |
| attribute_info | attributes(附加屬性的表) | attributes_count |
魔術用來判斷該文件是否是Class類文件。
常量池的個數從1開始計數,所以常量池的個數為nstant_pool_count-1。常量池主要存放兩大類常量,字面量以及符號引用。符號引用包括:類和接口的權限定名,字段名稱和描述符,方法的名稱和描述符。常量池的每一項表都是一個表,中共有11中表,具體可以看《深入理解java虛擬機》Page146,上面很詳細的介紹而這11中常量,字面量的結構都是一個u1長度的tag,表示這個常量的類型,一個u2長度的length,表示這個常量的長度,以及length個u1長度的bytes(u1,u2,u4,u8分別代表1個字節,2個字節,4個字節,8個字節)。
常量池后面緊接着是類的訪問權限控制符,類以及父類的全限定名,以及接口的個數,之后是接口的全限定名,全限定名都是指向常量池的符號引用。
再下面就是字段的個數,以及相應個數的表示字段的表,字段表的結構為:
| 類型 | 名稱 | 數量 |
| u2 | access_flag(字段修飾符) | 1 |
| u2 | name_index(字段的簡單名稱) | 1 |
| u2 | descriptor_index(字段的描述符) | 1 |
| u2 | attributes_count (字段的額外屬性的個數) | 1 |
| attribute_info | attributes(字段的額外屬性) | attributes_count |
全限定名:com/froest/TestClass;把comm.froest.TestClass中的"."換成"/",並且在最后加上";"就成為了全限定名,簡單名稱就是域的名稱或者方法的名稱;比如有方法 int getList(int a,char b,long c),那么該方法的描述符為:(ICJ)I;I為int類型的描述符,C為char類型的描述符,J為long類型的描述符,參數列表用"()",最后加上返回值的,描述符。
方法表的結構和字段表一樣
在Class文件、字段、方法表中都可以攜帶自己的屬性表結合,用於描述某些場景專有的信息。虛擬機預定義的屬性如下表所示:
| 屬性名稱 | 使用位置 | 含義 |
| Code | 方法表 | java代碼編譯成的字節碼指令 |
| ConstantValue | 字段表 | final關鍵字定義的常量值 |
| Deprecated | 類、方法表、字段表 | 被聲明為Deprecated的方法和字段 |
| Exceptions | 方法表 | 方法拋出的異常 |
| InnerClasses | 類文件 | 內部類列表 |
| LineNumberTable | Code屬性 | java源碼的行號和字節碼指令的對應關系 |
| LocalVariableTable | Code屬性 | 方法的局部變量描述 |
| SourceFile | 類文件 | 源文件名稱 |
| Synthetic | 類、方法表、字段表 | 表示方法或字段為編譯器自動生成 |
下面具體講下Code屬性,其他屬性可以在《深入理解java虛擬機》中找到。Code屬性的表結構如下:
| 類型 | 名稱 | 數量 |
| u2 | attribute_name_index(指向常量池中的”Code“常量,表示這個是"Code"屬性) | 1 |
| u4 | attribute_length("Code"屬性的長度) | 1 |
| u2 | max_stack(操作數棧的最大深度) | 1 |
| u2 | max_locals(局部變量表的最大空間,以slot為一個基本單位) | 1 |
| u4 | code_length(方法的字節碼指令的長度) | 1 |
| u1 | code(方法的字節碼指令) | code_length |
| u2 | excepion_table_length(方法體重用try-catch捕獲的異常類型的個數) | 1 |
| exception_info | exception_table(方法體重用try-catch捕獲的異常類型) | excepion_table_length |
| u2 | attributes_count(方法表的屬性的個數) | 1 |
| attribute_info | attributes(方法表的屬性) | attributes_count |
其中max_locals不一定是所有的局部變量的總和,因為有些局部變量是有作用域的,離開了作用域,這個局部變量就失去了作用,他所占用的slot也就可以被重用,所以max_locals可以小於等於方法中的所有的局部變量的總和。字節碼指令只占用一個字節,用u1表示。局部變量的順序,按照this,參數,局部變量。也就是第一個slot用來存放this(指向常量池中該類的符號引用,是一個地址),參數在局部變量中從第2個slot開始存放。
二、類加載機制
類加載按加載,連接,初始化這個順序進行的,其中連接又可以細分為驗證,准備,解析三個階段,部分解析可以在初始化開始之后再開始,這樣可以支持java的運行時綁定。雖然部分解析可以在初始化階段開始以后再開始,但是這部分的初始化還是需要當前的部分解析以后才可以初始化。java虛擬機規范中嚴格規定了有且之友中情況必須立即對類進行初始化:
1)遇到new創建實例,getstatic獲取類的靜態字段,putstatic設置靜態字段,invokestatic調用類的靜態方法
2)用java.lang.reflect包方法對類進行反射調用的時候,如果這個類沒有初始化過,那么先觸發其初始化
3)初始化一個類的時候,如果父類沒有進行初始化,那么必須先觸發其父類的初始化
4)當虛擬機啟動的時候,需要指定一個執行的主類,虛擬機會先初始化這個主類
用new關鍵字創建數組不會觸發相應的類初始化。調用一個類的靜態常量也不會觸發該類的初始化,因為調用類在編譯階段就已經把常量轉化為對自己的常量池的引用,例:
1 class ConstClass { 2 static { 3 System.out.println("ConstClass init"); 4 } 5 public final static String HELLODWORLD = "hello world"; 6 } 7 8 public class NotInitialization { 9 public static void main(String[] args) { 10 System.out.println(ConstClass.HELLODWORLD); 11 } 12 }
加載階段是整個類加載階段的第一個階段,在加載階段主要完成3件事情:
1)通過類的全限定名來回去定義此類的二進制流
2)將這個二進制流所代表的靜態存儲結構轉化為方法區的運行時數據結構
3)在java堆中生成一個代表這個類的java.lang.Class對象,作為方法區這些數據的訪問入口。
驗證階段是連接階段的第一步,則以不的目的是為了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。驗證階段分為4中驗證:Class文件格式的驗證,元數據的驗證,字節碼的驗證,符號引用驗證。Class文件格式驗證為了驗證是否符合Class文件的格式;元數據驗證是為了對類的元數據信息進行語義校驗,保證不存在不符合java語義規范的元數據信息;字節碼驗證主要是對方法體中的字節碼進行校驗分析;符號引用驗證主要是為了給解析階段符號引用轉化為直接引用做准備,對類自身以外的信息(常量池中的各種符號引用)進行匹配性校驗。
准備階段是正式為類變量(被static修飾的變量)分配初始值。
public static int a = 123;//類變量在准備階段初始化的值為0,而在初始化階段,在<cinit>構造方法中會把a的值初始化為123
public static final int a = 123;//用final修飾的類變量在准備階段,會把a的值初始化為123
解析階段就是把虛擬機在常量池中的符號引用替換為直接引用的過程,解析動作主要針對類或接口、字段、類方法、接口方法四類符號引用。
類或接口的解析,假設當前代碼所屬的類為D,如果要把一個從未解析過的符號引用N解析為一個類或接口C的直接引用,那么需要按下面的步驟執行:
1)如果C不是數組類型,那么虛擬機會把代表N的全限定名傳給D的類加載器去加載這個類C,加載過程,由於元數據、字節碼驗證的需要,又可能觸發其他相關的類的加載動作,一旦加載過程拋出任何異常,解析過程就會失敗。
2)如果C是個數組類型,並且數組的元素類型為對象,N的描述符為“[Ljava.lang.Integer”的形式,那么將會按照第一點規則加載數組的元素類型,接着有虛擬機生成一個代表此數組圍堵和元素的數組對象。
3)如果上述步驟沒有出現任何異常,那么C在虛擬機中已經成為一個有效的類了,但是在解析完成前還要確認C是否具備對D的訪問權限。
字段解析,假設字段所屬的類為C(字段表中的class_index屬性表示常量池中的class的全限定名):
1)在C中查找是否有簡單名稱和描述符都相同的字段,如果有,返回這個字段的直接引用,查找結束
2)如果C實現了接口,那么會按照繼承關系(接口可以繼承多個接口)從上往下遞歸搜索各個接口以及它的父接口,如果找到,查找結束
3)如果C不是Object類的話,將會按照繼承關系從上往下遞歸搜索其父類,如果找到,查找結束
4)否則查找失敗
如果找到了,那么驗證將會驗證這個字段的權限
類方法解析,假設這個方法所在的類為C(方法表中的class_index表示常量池中的class的全限定名)
1)如果在類方法表中發現這個class_index中的所以C是個接口,查找失敗
2)如果在C類中找到了(方法的簡單名稱和描述符一致),返回這個方法的直接引用,查找結束
3)否則,在C的父類中遞歸查找,如果找到,返回這個方法的直接引用,查找結束
4)否則,在C的接口列表以及他們的父接口中遞歸查找,如果找到,返回這個方法的直接引用,查找結束
5)否則,查找失敗
如果找到了,驗證是否有權限。
接口方法解析,和類方法解析類似,只是第一步不一樣,接口方法解析的第一步為:如果在類方法表中發現這個class_index中的所以C是個類,查找失敗。
初始化過程是執行類構造器<cinit>()方法的過程,<cinit>()會字段收集類中的所有類變量以及靜態語句塊(static{}),在初始化<cinit>()方法的時候,虛擬機會自動調用父類的<cinit>()方法,接口的<cinit>()方法可以到使用的時候在去初始化,虛擬機會保證<cinit>()方法在多線程環境先被正確的加鎖和同步。還有一個<init>()方法,這個方法是實例構造器,在創建實例的時候會被調用並且初始化。
任意一個類,都需要加載它的類加載器和這個類本身一同確定其在java虛擬機中的唯一性。
1 package com.froest.excel; 2 3 import java.io.InputStream; 4 5 public class ClassLoaderTest { 6 public static void main(String[] args) throws Exception { 7 ClassLoader myClassLoader = new ClassLoader() { 8 9 @Override 10 public Class<?> loadClass(String name) throws ClassNotFoundException { 11 try { 12 String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; 13 InputStream is = getClass().getResourceAsStream(fileName); 14 if (is == null) { 15 return super.loadClass(name); 16 } 17 byte[] b = new byte[is.available()]; 18 is.read(b); 19 return defineClass(name, b, 0, b.length); 20 } catch (Exception e) { 21 throw new ClassNotFoundException(name); 22 } 23 } 24 25 }; 26 Object obj = myClassLoader.loadClass("com.froest.excel.ClassLoaderTest").newInstance(); 27 System.out.println(obj.getClass()); 28 System.out.println(obj instanceof com.froest.excel.ClassLoaderTest); 29 } 30 }
上面代碼執行的結果為:
class com.froest.excel.ClassLoaderTest
false
第一個輸出表示obj確實是com.froest.excel.ClassLoaderTest實例化出來的對象,但是第二個類型檢查確實false,這是因為虛擬機的內存中有兩個ClassLoaderTest類,一個是應用程序加載器加載的,另外一個是我們自定義的類加載器加載的,雖然是同一個Class,但是還是獨立的兩個類。
類加載器使用雙親委派模型,這樣要加載一個類,首先查找這個類是否已經被加載過,如果沒有,那么類加載器會把這個類委派給這個加載器的父類去進行加載,如果父類不能加載,那么再自己加載。
三、字節碼執行
首先看一個數據結構---棧幀,棧幀是用於支持虛擬機進行方法調用和方法執行的數據結構,他是虛擬機運行時數據區的虛擬機棧的棧元素。棧幀中從上到下一次存儲了局部變量表,操作數棧,動態鏈接,返回地址等信息,每個方法從調用開始到調用結束,都對應着一個棧幀從入棧到出棧的過程。一個線程中只有棧頂的棧幀才是有效的,稱為當前棧幀,這個棧幀所關聯的方法就是當前方法。
操作數棧中的元素的數據類型要與字節碼指令的序列完全一致。
經過優化的虛擬機令兩個棧幀的局部變量表重疊一部分,公用一部分數據,這樣可以減少額外的參數的復制傳遞了。
動態鏈接就是在運行期間把符號引用轉化為直接引用的過程,相對於靜態解析(在加載階段的解析階段把符號引用轉化為直接引用)
方法的返回地址,方法返回有兩種類型,一種是正常完成出口,另一種是異常完成出口,方法退出的過程等同於棧幀出棧,因此棧幀出棧的時候可能執行的操作有:恢復上層調用方法的局部變量表和操作數棧,把返回值壓入調用方法的操作數棧中,調整PC計數器的值以執行方法調用指令的后一條指令等。
java虛擬機中的調用指令有invokestatic(調用靜態方法),invokespecial(調用實例構造器(<init>()方法),私有方法,父類方法),invokevirtual(調用所有的虛方法),invokeinterface(調用接口方法,會在運行時再確定一個實現此接口的對象)。只要能被invokestatic和invokespecial指令調用的方法,這些方法叫做非虛方法,都可以在解析階段確定唯一的調用版本,這種方法在類加載的時候就會符號引用解析為直接引用。相反的被invokevirtual和invokeinterface指令調用的方法叫做虛方法(除了final方法,因為final方法不允許被修改,只有一種形式)。
靜態分派:所有依賴靜態類型來定位方法執行版本的分派動作都稱為靜態分派,靜態分派的典型應用就是方法重載。
動態分派:在運行期根據實際類型確定方法的執行版本的分派過程稱為動態分派,動態分派的典型應用就是方法重寫。
宗量:方法的接受者和方法的參數統稱為方法的宗量。
單分派:根據一個宗量對目標方法進行選擇
多分派:根據多個宗量對目標方法進行選擇
java是一種靜態多分派,動態單分派語言。
類的方法區會保存一張虛方法表,存放方法的實際入口地址,如果沒有重寫父類的方法,那么入口與父類的一樣,如果重寫了父類的方法,那么方法的入口地址指向自己的方法入口地址。方法表一般在類加載的連接階段進行初始化,准備了類變量的初始值之后,虛擬機會把該類的方法表也初始化完畢,這是java實現動態分派方法。
