標籤:類型轉換 開始 不可 address 完成後 init 範圍 情況 over
一.程式儲存格式
- 統一的程式儲存格式:不同平台的虛擬機器於所有平台都統一使用程式儲存格式——位元組碼(ByteCode);
- JAVA 虛擬機器不關心 Class 檔案的來源,而只和“Class檔案"這種二進位檔案格式關聯,也就是說Java虛擬機器只認識“Class"檔案;
- Java 編譯器可以把 Java 程式碼編譯成虛擬機器所需要的Class 檔案;
二.Class 檔案結構
Class 檔案是以 8 個位元組為單位的二進位流,緊湊排列,中間沒有空隙;如果想查看一個 Class 檔案除了通過 winHex 編譯器看到位元組碼,也可以通過 javap -verbose xxx.Class 輸出位元組碼內容,這樣看起來比較直觀。
1、基本類型
無符號數:
- 定義:基本的資料類型,u1、u8表示1個位元組,8個位元組。
- 使用:可以用來描述數字、索引、引用,utf-8格式的字串;
表:
- 定義:多個無符號數和其他表組成的複合資料型別;通常以“_info” 結尾。
- 使用:描述有層次關係的複合結構資料;
2、魔數與版本
- 魔數:每個Class檔案的頭4個位元組,唯一作用就是確定這個檔案是否能被一個虛擬機器接受的Class檔案;
- 次版本號碼:緊接著魔數後面的第5和第6個位元組;
- 主要版本號:第7和第8個位元組代表主要版本號,比如說50對應的就是JDK1.6.
- 可以使用十六進位編譯器WinHex開啟一個Class檔案瞅瞅;
3、常量池
版本號碼之後緊跟的就是常量池入口,可以理解為Class檔案之中的資源倉庫;
- 常量池容量計數器:u2類型,代表本Class檔案有N-1個常量(因為是從1開始技術的);
- 0項常量:不引用任何一個常量池項目
- 字面量:文本字串、final常量值等;
- 符號引用
- 類和介面的全限定名
- 欄位的名稱和描述符
- 方法的名稱和描述符
- 定義:UTF-8編碼的字串長度是多少個位元組;
- 65535限制:Class檔案中方法、欄位等都需要引用CONSTANT_ Utf8_ info型常量的length為u2類型,最大為65535.如果某個變數或者方法名超過了64K,那麼這個length容不下了,當然也就無法編譯了。
4、訪問標誌(access_flags)
- 常量池之後緊跟的就是訪問標誌。主要包括了這個Class是類or介面,是不是 public,是不是 abstract 類型,是不是 final 類型。
5、類索引、父類索引和介面類集合
- java.lang.Object類索引為0;
- 類的索引其實就是描述了這個Class的extends和implements的關係;
6、欄位表集合(field_info)
- 用於描述定義的變數,依次包括了訪問標誌(access_ flags)、名稱索引(name_ index)、描述索引(descriptor_ index)、屬性工作表集合(attributes)。
- 描述的資訊如下:
-
- 範圍:public、private、protect
- 執行個體or類變數:static
- 可變性:final
- 並發可見度:volatile
- 是否可序列化:transient
- 欄位資料類型:基本類型、對象、數組等
- 欄位名稱;
-
- 1、不會列出超類or父類或者父介面繼承而來的欄位;
- 2、有可能列出原本Java代碼中不存在的欄位(內部類會自動添加指向外部類執行個體的欄位,才能引用到外部類);
- 3、Java語言中欄位是無法重載的;
7、方法表集合
和欄位表集合差不多,方法表集合用來描述Class檔案中的方法,但是訪問標誌和屬性工作表集合和欄位表集合有所區別;
- 訪問標誌:
- volatile、transient關鍵字不可以修飾方法,方法表中少了這兩種標誌;
- synchronized、native、strictfp和abstract可以修複方法,故方法表增加了這些對應的標誌;
- Code屬性:
- 方法表集合原則
- 方法沒有重寫(Override),父類的資訊不會寫到子類的方法表中;
- 編譯器有可能自動添加一些方法,典型的如類構造器的“< clinit >”、方法&執行個體構造器的“< init >“方法;
- 重載(Overload)一個方法,需要添加一個特徵簽名,特徵簽名就是一個方法中各個參數在常量池中的欄位符號引用的集合;
8、屬性工作表集合(attribute_info)
上述那些表需要攜帶自己的某些屬性,來描述自己的特殊環境資訊,比如InnderClasses、LineNumberTable、Code 之類的;
- Code (用語描述代碼)
- max_stack:操作棧深度最大值,JVM 運行時根據這個值來分配棧幀(Stack Frame)中的操作棧深度;
- max_locals:代表了局部變數表所需要的儲存空間。
- Slot:虛擬機器為局部變數分配記憶體的最小單位
- byte、char、float、int、short、boolean、returnAddress 長度少於32位,佔1個slot
- double、long 64位,佔2個slot
- 當代碼超出一個局部變數的範圍時,這個局部變數所佔用的 slot 可以被其他的局部變數所使用
- code_length:位元組碼長度
- code:儲存位元組碼指令
- 65535限制:虛擬機器規定了一個方法不允許超過 65535 條位元組碼,否則編譯不通過;
- 執行:執行過程中的資料交換、方法調用等操作都是基於棧(操作棧)的;
- this關鍵字:在執行個體方法中通常可以有個 this 關鍵字來引用當前對象的變數,這是因為 Java 編譯時間在局部變數表中自動增加了這個(this)局部變數。
- LineNumberTable:描述 Java 的源碼行號和位元組碼行號;
- LocalVariableTable:描述局部變數表中的變數與Java源碼中定義的變數之間的關係;
三.位元組碼指令
1、位元組碼組成
- 作業碼(Opcode):i(助記符)代表int類型資料操作....等等;
- 運算元 (Operands):永遠都是一個數群組類型的對象;
Java虛擬機器採用面向運算元棧而不是寄存器的架構,位元組碼指令集是一種指令集架構。放棄了運算元對齊,省略了填充的符號和間隔。
2、載入和儲存指令
將資料在幀棧中將局部變數表和運算元棧之間來回傳輸。
- 將一個局部變數載入到操作棧;
- 將一個數值從運算元棧儲存到局部變數表;
- 將一個常量載入到運算元棧;
- 擴充局部變數表的訪問索引的指令;
3、運算指令
- 將兩個運算元棧上的值進行某種特定運算,並把結果重新存入到操作棧頂;
- Java沒有直接支援byte、short、char、boolean類型,都轉為int類型進行運算,使用int的指令代替;
4、類型轉換指令
- 寬化轉換
- int到long、float、double
- long到float、double
- float到double
- 窄化轉換
- 必須顯示的聲明轉換
- 有溢出或者丟精的情況,但不會拋出異常
5、同步指令
- Java虛擬機器支援方法級同步和方法內部一段指令序列同步,這兩種同步都是通過“管程”來支援;
- 執行線程就要求先成功持有“管程”,然後才能執行方法,最後方法執行完成後,才釋放“管程”。
- Java虛擬機器通過 monitorenter 和 monitorexit兩個指令配對使用,另外編譯器會自動增加一個異常處理器。當出現異常時,這個異常處理器能夠捕獲到所有的異常,並且釋放“管程”,monitorexit 指令響應。這樣的話,保證了 monitorenter 和monitorexit 總是成對出現的。
三.代碼舉例
1、Java檔案:
1 package com.xxx.ccc; 2 public final class InitConfig { 3 public static final InitConfig BFCACCOUNT = new InitConfig(0, "aaa", "AAA"); 4 private int mIndex; 5 private String mData; 6 private String mDescribe; 7 private InitConfig(int indexFlag, String data, String describe) { 8 this.mIndex = indexFlag; 9 this.mData = data;10 this.mDescribe = describe;11 }12 public String getmData() {13 return this.mData;14 }
2、Class 檔案:
1 Last modified 2017-7-4; size 1050 bytes 2 MD5 checksum 2beb0c10f91b793c3570edcf2d1eff78 3 Compiled from "InitConfig.java" 4 public final class com.xxx.xxx.InitConfig 5 minor version: 0 //次版本號碼 6 major version: 51 //主要版本號 7 flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER //訪問標誌 8 Constant pool: //常量池 9 #1 = Methodref #14.#41 // java/lang/Object."<init>":()V10 #2 = Fieldref #5.#42 // com/xxx/xxx/InitConfig.mIndex:I11 #6 = Class #46 // com/xxx/xxx/common/constant/ConstData12 #7 = String #47 // aaa13 #23 = Utf8 <init>14 #24 = Utf8 (ILjava/lang/String;Ljava/lang/String;)V15 #25 = Utf8 Code16 #26 = Utf8 LineNumberTable //Java的源碼行號和位元組碼行號17 #27 = Utf8 LocalVariableTable //局部變數表中的變數與Java源碼中定義的變數之間的關係18 #28 = Utf8 this19 #32 = Utf8 getmData20 #33 = Utf8 ()Ljava/lang/String;21 #37 = Utf8 <clinit>22 #38 = Utf8 ()V23 #40 = Utf8 InitConfig.java24 #41 = NameAndType #23:#38 // "<init>":()V25 #45 = Utf8 com/xxx/xxx/InitConfig26 #46 = Utf8 com/xxx/xxx/common/constant/ConstData27 #53 = NameAndType #17:#16 // SEAACCOUNT:Lcom/xxx/ccc/InitConfig;28 #54 = Utf8 java/lang/Object29 public static final com.xxx.xxx BFCACCOUNT;30 descriptor: Lcom/xxx/xxx/InitConfig;31 flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL 32 public java.lang.String getmData();33 descriptor: ()Ljava/lang/String;34 flags: ACC_PUBLIC35 Code:36 stack=1, locals=1, args_size=137 0: aload_038 1: getfield #3 // Field mData:Ljava/lang/String;39 4: areturn40 LineNumberTable: //Java的源碼行號和位元組碼行號41 line 36: 042 LocalVariableTable: //局部變數表中的變數與Java源碼中定義的變數之間的關係43 Start Length Slot Name Signature44 0 5 0 this Lcom/xxx/xxx/InitConfig; //方法裡面預設增加了個this
四.小結
為什麼說一些”非Java"語言也是可以在 JVM 上跑,這是因為 JVM 只認識 Class 檔案,所以如果某某語言最終編譯出的檔案是 Class 檔案,那麼對於 JVM 來說沒有什麼區別,但是得按照 Class 檔案的結構來,不然也無法正常執行。Class 定義了許多特定的基本類型和表結構,通過魔數讓 JVM 認識該檔案,版本號碼保證可以在要求的 JDK 版本上運行,在常量池中定義好常量,訪問標誌位確定存取權限。索引集合方便與外界的 class 保持聯絡,欄位表儲存我們定義好的變數,方法表格儲存體方法的資訊,屬性工作表儲存了上述各種表的一些屬性。其中記住 slot為局部變數分配記憶體的最小單位,當程式超出範圍的時候,slot 可以被其他替換使用。到這裡,僅僅是代碼最靜態儲存的格式,程式要運行起來。還需要操作指令,也是由位元組碼儲存,包括作業碼和運算元。有載入儲存、運算、類型轉換、同步指令。
深入理解Java虛擬機器04--類結構檔案