從零開始用 Rust 打造一個玩具級別 Java 虛擬機 (二) Class字節碼解析

上一章 咱們講解了類的加載,後面咱重新寫了代碼,咱打算用 未來可能很火的Rust 來完成這個項目。

.Class文件介紹

JAVA中每個class 文件就是一個類,類名和文件名相同, 按照Java虛擬機規範其中對類名有了嚴格的規定。Java虛擬機 對類的加載方式則較爲寬鬆 類文件可以是從.JAR .ZIP 文件中讀取加載class文件,甚至可以從網絡上加載。

Java 加載Class 流程:

Created with Raphaël 2.2.0前端編譯器編譯成.Class字節碼文件虛擬機裝載.Class文件虛擬機,解釋編譯爲 對應平臺上的機器指令運行

什麼是Java 字節碼文件 .Class?

按照《Java虛擬機規範 JavaSE7版》的描述來看,任何編程語言的編譯結果滿足幷包含Java虛擬機的內部指令集、符號表以及一些其他的輔助信息,它就是一個有效的字節碼文件,就能夠被虛擬機所識別並裝載運行

什麼是字節碼、機器碼、本地代碼?

字節碼是指平常所瞭解的 .class 文件,Java 代碼通過 javac 命令編譯成字節碼

機器碼和本地代碼都是指機器可以直接識別運行的代碼,也就是機器指令

字節碼是不能直接運行的,需要經過 JVM 解釋或編譯成機器碼才能運行

此時你要問了,爲什麼 Java 不直接編譯成機器碼,這樣不是更快嗎?

  1. 機器碼是與平臺相關的,也就是操作系統相關,不同操作系統能識別的機器碼不同,如果編譯成機器碼那豈不是和 C、C++差不多了,不能跨平臺,Java 就沒有那響亮的口號 “一次編譯,到處運行”;

  2. 之所以不一次性全部編譯,是因爲有一些代碼只運行一次,沒必要編譯,直接解釋運行就可以。而那些“熱點”代碼,反覆解釋執行肯定很慢,JVM在運行程序的過程中不斷優化,用JIT編譯器編譯那些熱點代碼,讓他們不用每次都逐句解釋執行;

  3. 還有一方面的原因是後文講解的解釋器與編譯器共存的原因。

什麼是 JIT ?

爲了提高熱點代碼的執行效率,在運行時,虛擬機將會把這些代碼編譯成與本地平臺相關的機器碼,並進行各種層次的優化,完成這個任務的編譯器稱爲即時編譯器(Just In Time Compiler),簡稱 JIT 編譯器

什麼是編譯和解釋?

編譯器:把源程序的每一條語句都編譯成機器語言,並保存成二進制文件,這樣運行時計算機可以直接以機器語言來運行此程序,速度很快;

解釋器:只在執行程序時,才一條一條的解釋成機器語言給計算機來執行,所以運行速度是不如編譯後的程序運行的快的;

通過javac命令將 Java 程序的源代碼編譯成 Java 字節碼,即我們常說的 class 文件。這是我們通常意義上理解的編譯。

字節碼並不是機器語言,要想讓機器能夠執行,還需要把字節碼翻譯成機器指令。這個過程是Java 虛擬機做的,這個過程也叫編譯。是更深層次的編譯。(實際上就是解釋,引入 JIT 之後也存在編譯)

此時又有疑惑了,Java不是解釋執行的嗎?

沒錯,Java 需要將字節碼逐條翻譯成對應的機器指令並且執行,這就是傳統的 JVM 的解釋器的功能,正是由於解釋器逐條翻譯並執行這個過程的效率低,引入了 JIT 即時編譯技術。

必須指出的是,不管是解釋執行,還是編譯執行,最終執行的代碼單元都是可直接在真實機器上運行的機器碼,或稱爲本地代碼

在這裏插入圖片描述

字節碼結構組成?

.Class字節碼組成有不通長度的字節碼(Byte)爲單位通過拼湊而成,Java虛擬機規範定義了u1、u2和u4三種數據類型來表示1、 2和4字節無符號整數, 並且以大端(big-endian)方式存儲。

類型 名稱 數量
u4 magic(魔數) 用來標識讓虛擬機識別這個是Class文件 固定0xCAFEBABE 1
u2 minor_version 編譯當前Class的jdk次版本號,高版本jdk編譯文件不能運行在低版本JVM上 反之可以 1
u2 major_version 編譯當前Class的jdk主版本號 1
u2 constant_pool_count 標識常量池 包含常量個數 1
cp_info constant_pool 常量池 constant_pool_count -1
u2 access_flags 訪問標誌,某個類或者接口的訪問權限 public final abstract 等都是訪問標誌 1
u2 this_class 通過索引指向常量池中類的全限定名 1
u2 suprer_class 所有類默認超類爲Object,並且一個類只能有一個超類 1
u2 interfaces_count 繼承接口的個數,從0開始 1
u2 interfaces 接口表,接口名全限定名在常量池中的索引 interfaces_count
u2 fields_count 類變量和實例變量的總和 1
field_info fields_count個field_info結構體組成的 記錄實例變量和類變量的一些信息 比如 是否被 public final 修飾等 fields_count
u2 methods_count 記錄當前類的方法有多少個 1
method_info methods_count個method_info結構體組成的 方法表 就是記錄一些 方法的 修飾符 public final 信息 methods_count
u2 attribute_count 表示attribute_info個數 1
attribute_info attribute_count 個 attribute_info組成指向 attribute_info包含索引指向常量池的CONSTANT_Utf8_info 也就是一個utf8 字符串 attribute_count

ps:u2 = 2byte ,u4 = 4byte ,u8 = 8byte (無符號整數)

魔數

在這裏插入圖片描述
魔數很簡單,其實就是你給你的名字象徵的七個表示在前幾個字節,java 文件字節碼以 0xCAFEBABE 開頭 那麼 虛擬機就判斷 這時個.Class 文件 能被虛擬機加載,其實計算機很多格式的文件 比如.AVI .JPG .MOV 這樣的文件開頭前幾個字節都是固定魔數。而 0xCAFEBABE 對應ASCII碼 則是 cafe babe,關於Java 起名的由來是 取自某座盛產咖啡的島爲命名的。

主版本號次版本號

主版本號 就你編譯是編譯JDK的版本號, 主要是讓虛擬機能判斷 如果是低版本的虛擬機,沒法 運行高版本的 JDK編譯的 .Class文件的。

JDK版本號 Class版本號 16進制
1.1 45.0 00 00 00 2D
1.2 46.0 00 00 00 2E
1.3 47.0 00 00 00 2F
1.4 48.0 00 00 00 30
1.5 49.0 00 00 00 31
1.6 50.0 00 00 00 32
1.7 51.0 00 00 00 33
1.8 52.0 00 00 00 34

在這裏插入圖片描述
可以看到 00 34 對應JDK版本號是1.8。

常量池的數據結構:

緊跟在 主版本號,後面的是 常量池的數量,值的注意的一點是 常量池的索引是從1開始的,所以實際上 是常量數 constant_pool_count - 1, 當某個 屬性指向常量池0號索引 表示不引用當前常量池的內容.

常量池裏麪包含了很多的數據結構,是這些數據結構的集合,可以用來存儲 字符串常量值 類名 接口名 字段和方法名等等。常量池按結構可以分爲兩大常量:字面量(Literal) 和 符號引用(Symbolic References)。 字面量 如文本字符串、整數浮點數值等。
而符號引用 屬於編譯原理的概念、包括三類常量:

  • 類和接口的全限定名
  • 字段的名稱和描述符
  • 方法的名稱和描述符

在這裏插入圖片描述

//class 讀取後的完整結構
#[derive(Debug, Clone)]
pub struct ClassFile {
    pub magic: u32, //class文件的魔數
    pub minor_version: u16, //編譯此Class文件JDK的次版本號
    pub major_version: u16, //編譯此Class文件JDK的主版本號
    pub constant_pool_count: u16,//常量池數量 從 1開始 0保留
    pub constant_pool: Vec<Constant>,//常量池 包含14中結構的數據
    pub access_flags: u16, //訪問標誌 記錄 接口類 方法的 訪問標誌
    pub this_class: u16,//當前類名的索引 指向常量池一個utf-8字符串
    pub super_class: u16,//父類的索引 除了 java.lang.Object 每個類都應該有對應的父類,指向常量池一個utf-8字符串
    pub interfaces_count: u16,//接口的個數
    pub interfaces: Vec<Constant>,//常量池 utf-8結構 存放 繼承接口的類名 從左到右
    pub fields_count: u16,//字段數
    pub fields: Vec<FieldInfo>,//字段表信息 存放變量的 名字描述符和 屬性
    pub methods_count: u16,//方法樹
    pub methods: Vec<MethodInfo>,//存放方法的名 描述符合屬性
    pub attributes_count: u16,//屬性數
    pub attributes: Vec<AttributeInfo>,//記錄屬性表
}

在Java Se7 中包含 14中常量池的的格式,但是 它們之間格式之間 有相似性,都會有一個 tag 表示 常量池的數據類型。和 info 存放實際數據。

type cp_info struct {
	tag uint8
	info[] uint8
}

tag 用來表示14中結構體的編號。
對照上面的表,每次你讀常量池時先讀1個字節 知道 確定後面需要讀取的數據的結構,按對應的讀取方法再做讀取。

常量池14種結構

全限定名

全限定名和簡單名稱其實解釋 類的名字 比如 java.lang.Object 這個 所有類的父類, 它的全限定名稱 就是 java/lang.Object; 僅僅是把 “.” 替換成了 “/” 並且最後加入了一個 “;” 符號。

描述符、簡單名稱:

什麼是描述符呢?簡單來說 就是 起了個簡化名來表示 方法的參數,類型等,描述符存在常量池裏,用的是 改良版的utf-8格式,那麼很多小夥伴要有疑問了 爲什麼好好的名字不用 非要縮略呢,其實就是爲了 壓縮文件大小,讀取更快嗎,原來一個 byte 用B 來表述 計算機還是能識別,但是隻用了一個字節 .

自段描述符:

在這裏插入圖片描述
使用描述符 一個Object對象被標示成了 Ljava/lang/Object ,一個 一維數組可以表示成[,二維數組被表示成[[ ,java 規定了 數組最大維度 255。

方法描述符

方法描述符(method descriptor)包含0個或多個參數描述符(parameter descriptor) 以及1個返回值描述符(return descriptor)。參數描述符表示表示該方法鎖接受的參數類型,如果該方法有返回值的話,那麼該返回值描述符則表示該方法的返回值的類型。

舉個例子:
Objcet m(int I,double d,Thread t){…}

它 有三個參數, 分別對應的描述符爲 int ->I, double ->D, Thread -> Ljava/lang/Thread 返回值爲 Objcet -> Ljava/lang/Object

那麼 最後轉化爲描述符爲 (IDLjava/lang/Thread;) Ljava/lang/Object

一個有效的 方法描述符,參數數量 控制在 255個以內,對於 實例方法 來說 就是 對象的方法 this.xxx()
實際上 當你調用 對象.xxx() 的時候本質上 就是 將 對象this傳入了 這個方法。
所以 實例方法 本身 this 也作爲一個參數,傳遞 this 不是由 方法描述符 來記錄的,也就是說 參數 this 的傳遞是由調用實例方法的 指令來傳遞的,還有 要注意的一點是 每個double 或 long 佔用 2個參數長度。

總結:類型描述符。
①基本類型byte、short、char、int、long、float和double的描述符是單個字母,分別對應B、S、C、I、J、F和D。注意,long的描述符是J而不是L。
②引用類型的描述符是L+類的完全限定名+分號。
③數組類型的描述符是[+數組元素類型描述符。
2)字段描述符就是字段類型的描述符。
3)方法描述符是(分號分隔的參數類型描述符)+返回值類型描述符,其中void返回值由單個字母V表示。

CONSTANT_Utf8_info結構

/**
    -  tag 1
    -  length u2(UTF-8改良版字符串的長度 最長65535)
    -  bytes length(存儲的UTF8字符串 用來被存儲 方法名、類的全限定名、字段名、方法參數描述符等)
    -  *sample:  <init> | Ljava/lang/String; | args | ([Ljava.lang.String;)V | [[C (二維char數組)
     */
    CONSTANTUtf8Info {
        length:u16,
        bytes:Vec<u8>,
    }
  • tag:1
  • length:用來指明bytes的長度,length 最長 是 65535 所以 變量名、String類型長度、方法名 參數名等 最長 是 65535byte = 64KB
    bytes[] :由length個長度組成的,使用改進的UTF-8 編碼個是來存儲,與傳統的UTF-8編碼區別在於,改進過的UTF-8 編碼格式從’\u0001’ ~ ‘\u007f’ 之間的字符使用1個字符來表示,從’\u0080’ ~’\u07ff’ 之間的字符使用2個字節來表示;而從’\u0800’ ~’\ufff’之間的字符則與傳統的 UTF-8 編碼格式一樣使用 3個字節來表示。

在這裏插入圖片描述

CONSTANT_Class_info 結構

指向Utf8_info 表示接口

/**
     -  tag 7
     -  name_index u2(名字,指向utf8)
     -  descriptor_index u2(描述符類型,指向utf8)
     -  *sample: new String();
     -> name_index -> utf8(#name_index)  -> 指向的 是一個類或接口的全限定名 java/lang/String
     */
    CONSTANTClassInfo {
        name_index: u16,
    }
  • tag :7
  • name_index 爲對常量池表的一個索引,其索引的對象爲
    CONSTANT_Utf8_info結構,是一個 使用改進的UTF-8格式存儲的字符串,在這裏是用來存放類 或接口的全限定名字符串。
    在 JVM中 包含三種引用類型,分別是:類類型(class type)、數組類型(array type)和 接口類型(interface type),CONSTANT_Class_info 也可用來表示數組類型 、一個char數組類型描述符用 [C 表示

CONSTANT_Fieldref_info、 CONSTANT_Methodref_info 和、CONSTANT_InterfaceMethodref_Info 結構

字段、方法和接口方法結構如下

//字段引用

/**
    -  字段引用信息
    -  tag  9
    -  class_index u2(指向CONSTANT_Class 當前字段所屬到的所屬的類或接口)
    -  name_and_type_index u2 (指向 CONSTANT_NameAndType 獲得當前字段的簡單名稱和自段描述符)

    - *sample :public class HelloClass {
                private static String hellocclass;//變量名、字段名= hellocclass 變量類型:String 描述符形式就是 Ljava/lang/String;
                public static void main(String[] args) {
                    hellocclass =".CLASS";
                    }
                }
     - > class_index - > CONSTANT_Class(#class_index) -> utf8(CONSTANT_Class.name_index) -> HelloClass 當前字段所在類的類名
        - > name_and_type_index -> CONSTANT_NameAndType(#name_and_type_index) ->  utf8(#CONSTANT_NameAndType.name_index)   ->  hellocclass  ↓
                                                                              ->   utf8(#CONSTANT_NameAndType.#descriptor_index) -> Ljava/lang/String;  ↓
                                                                                                                              ----->  hellocclass:Ljava/lang/String; 字段名:字段的類型
    */
    CONSTFieldrefInfo {
        class_index: u16,
        name_and_type_index: u16,
    }
  • tag: 9
  • class_index : class_index 指向常量池的 CONSTANT_Class_info 結構, 被指向的CONSTANT_Class_info 即可以表示爲類、也可以表示爲接口,就是最終指向常量池的一個字符串可以是接口名,或者是類名。
  • name_and_type_index:指向常量池的CONSTANT_NameAndType_info 結構,它表示當前字段描述符。

System.out.println() 在常量池裏 表示爲 CONSTANT_Fieldref_info 結構。2各部分 組成 類名 和 方法的非限定名。
在這裏插入圖片描述

//方法引用

 /**
       -  方法引用信息
       -  tag  10
       -  class_index u2(指向CONSTANT_Class 一個類的全限定名)
       -  name_and_type_index u2 (指向 CONSTANT_NameAndType 方法的名字、參數的描述符)
       -  *sample: new String();
       - > class_index - > CONSTANT_Class(#class_index) -> utf8(CONSTANT_Class.name_index) -> java/lang/String 類的全限定名
       - > name_and_type_index -> CONSTANT_NameAndType(#name_and_type_index) ->  utf8(#CONSTANT_NameAndType.name_index)   ->   <init>   ↓
                                                                             ->   utf8(#CONSTANT_NameAndType.#descriptor_index) -> ()V  ↓
                                                                                                                   ----->  <init>()V 調用init方法 無參、返回值void
       */
        CONSTMethodrefInfo {
            class_index: u16,
            name_and_type_index: u16,
        }
  • tag: 10
  • class_index : class_index 指向常量池的 CONSTANT_Class_info 結構, 被指向的CONSTANT_Class_info 表示爲類,說白了就是最終指向常量池裏的字符串,是個類名。
  • name_and_type_index:指向常量池的CONSTANT_NameAndType_info 結構,它表示當前方法的描述符。
    在這裏插入圖片描述
    上圖可以看到,當我們 new 一個 String 類型時 使用的 CONSTANT_Methodref_info 它 class_index 指向了一個 Class_info 指定了類的全限定名 java/lang/String , name_and_type_index : 指定了 方法的名字 和 方法修飾符 包含 參數和返回值 ()V ,
    () 表示沒有參數 V代表 Void 沒有返回值。

//接口引用

在這裏插入圖片描述

/**
     -  接口引用信息
     -  tag  11
     -  class_index u2(指向CONSTANT_Class 當前方法所屬的接口)
     -  name_and_type_index u2 (指向 CONSTANT_NameAndType 獲得當前接口方法的簡單名稱和自段描述符)
     - *sample :public class HelloClass {
            //接口
            private static aaa myinferface;
            public static void main(String[] args) {}
            public String method1() {
                //調用結構方法的引用
                return myinferface.method1();
            }
        }
     - > class_index - > CONSTANT_Class(#class_index) -> utf8(CONSTANT_Class.name_index) -> aaa 引用方法所在的接口
     - > name_and_type_index -> CONSTANT_NameAndType(#name_and_type_index) ->  utf8(#CONSTANT_NameAndType.name_index)   ->  method1  ↓
                                                                           ->   utf8(#CONSTANT_NameAndType.#descriptor_index) -> ()Ljava/lang/String;  ↓
                                                                                               ----->  method1:()Ljava/lang/String; 接口方法名:接口方法的參數返回值描述符
                                                                                                                                返回 String 無參數
    */
    CONSTInterfaceMethodrefInfo {
        class_index: u16,
        name_and_type_index: u16,
    }
  • tag: 11
  • class_index : class_index 指向常量池的 CONSTANT_Class_info 結構, 被指向的CONSTANT_Class_info 表示爲 一個接口,最終指向一個字符串 表示爲 接口名。
  • name_and_type_index:指向常量池的CONSTANT_NameAndType_info 結構,它表示當前方法的描述符。

CONSTANT_String_info 結構

CONSTANT_String_info 結構表示String類型的常量對象。

/**
    -  tag  8
    -  string_index u2(指向utf8的索引)
    */
    CONSTStringInfo {
        string_index: u16,
    }

在這裏插入圖片描述
在這裏插入圖片描述

  • u1:8
  • string_index:對應一個 CONSTANT_Utf8_info 結構在常量池中索引。

這個結構最後會轉初始化爲一個String 對象。

CONSTANT_Integer_info 和 CONSTANT_Float_info 結構

/**
    - tag 3
    - bytes u2(大端直接存儲整形值,佔用4個字節)
    - *sample:  public static final int a =50;
    -> 注意:只有被final 修飾的纔會在 編譯時就加入常量池。
    */
    CONSTANTIntegerInfo {
        i: i32,
    }

在這裏插入圖片描述

圖上我們可以看到,short 其實 也是使用 CONSTANT_Integer_info 這個結構來存儲的。

  • tag : 3
  • bytes:表示4個字節的整數,按大端存儲。
/**
    - tag 4
    - bytes u2(大端直接存儲浮點值,佔用4個字節)
    - *sample:  public static final float b =  0.1f;
    */
    CONSTANTFloatInfo {
        f: f32,
    }
  • tag : 4
  • bytes:按照IEEE 754單精度浮點格式來表示 float常量的值,同樣也是大端存儲(Big-endian)。
    在這裏插入圖片描述
    總結:從這裏 我們就可以看到 在java裏 int 佔用的 是 4個字節

CONSTANT_Long_info 和 CONSTANT_Double_info 結構

CONSTANT_Long_info、CONSTANT_Double_info 各自佔用 2個常量池索引。
在這裏插入圖片描述

 /**
    - tag 5
    - bytes u8(按照大端存儲一個佔8個字節的long長整型數,其實可以分爲 高四位 和低四位 通過 ((long)high_bytes << 32)  +  low_bytes 計算出實際數)
    - *sample:   public static final long c =  111111;
    -> 注意 一個Long類型 會在常量池佔2個索引。
    */
    CONSTANTLongInfo {
        i: i64
    }
  • tag:5
  • high_bytes、low_bytes:將高32位 左移 32位 加上 低 32位 ((long)high_bytes << 32) + low_bytes 合起來 就是一個 64位的long類型。
 /**
   - tag 6
   - bytes u8(按照大端存儲一個佔8個字節的long長整型數,其實可以分爲 高四位 和低四位 通過 ((long)high_bytes << 32)  +  low_bytes 計算出實際數)
   - *sample:   public static final double d =  111111.00;
   -> 注意:一個Double類型 會在常量池佔2個索引。
   */
    CONSTANTDoubleInfo {
        f: f64,
    }
  • tag:6
  • high_bytes、low_bytes 按照Ieee 754 雙精度浮點格式來表示 double常量的值。- - high_bytes 和 low_bytes 都按照大端(big-endian)順序存儲.
    ((long) high_bytes << 32 ) + low_bytes
    在這裏插入圖片描述

CONSTANT_NameAndType_info 結構

CONSTANT_NameAndType_info 結構用於表示字段或方法。

/**
    -  tag 12
    -  name_index u2(名字,指向utf8)
    -  descriptor_index u2(描述符類型,指向utf8)
    -  *sample: new String();
    -> name_index -> utf8(#name_index)  -> <init> 指向一個utf8 的方法名
    -> descriptor_index -> utf8(#descriptor_index) -> ()V 指向方法的參數和返回值的描述符 () = 沒有參數 V = Void
    */
    CONSTANTNameAndTypeInfo {
        name_index: u16,
        descriptor_index: u16,
    }
  • tag:12
  • name_index:對常量池的中CONSTANT_Utf8_info結構的有效索引,表示爲 或者有效的字段或方法的非限定名。
  • descriptor_index:對常量池的中CONSTANT_Utf8_info結構的有效索引,表示爲 字段描述符或方法 描述符。
    在這裏插入圖片描述
    在這裏插入圖片描述

CONSTANT_MethodHandle_info結構

用於表示方法的類型。

/**
   - tag 15
   - reference_kind 值在1~9之間,它決定了後續 reference_index項中的方法句柄類型,方法句柄的值表示方法句柄的字節碼行爲。
   - reference_index 指向常量值列表的有效索引
   -> 注意:同樣只有被final 修飾的纔會在 編譯時存入常量池,並且一個Double類型 會在常量池佔2個索引。
   */
    ConstantMethodHandleInfo {
        reference_kind: u8,
        reference_index: u16,
    }
  • tag 15
  • reference_kind 項的值必須在範圍1 ~ 9(包括1和9)之內,它表示方法句柄的類型(kind). 方法句柄類型決定句柄的字節碼行爲(bytercode behavior)
  • reference_index 項的值必須是對常量池表的有效索引.該位置上的常量池表項。

CONSTANT_MethodType_info結構

/**
   - tag 16
   - descriptor_index 指向CONSTANT_Utf8_info結構,表示方法的類型。
   
   */
    ConstantMethodTypeInfo {
        descriptor_index: u16,
    }
   
  • tag 16
  • descriptor_index 指向的是CONSTANT_Utf8_info,表示方法的類型。

CONSTANT_InvokeDynamicInfo

/**
    - tag 18
    - bootstrap_method_attr_index 對當前字節碼文件中引導方法的boostrap_method 數組的有效索引
    - name_and_type_index name_and_type_index 項的值則是一個指向常量池列表中CONSTANT_NameAndType_info常量項的有效索引,用於表示方法得的簡單名稱和方法描述符。
    */
    ConstantInvokeDynamicInfo {
        bootstrap_method_attr_index: u16,
        name_and_type_index: u16,
    }

訪問標誌

常量池結束後,接着是 訪問標誌(access flag)用來表示識別一些類或接口的訪問信息,比如 這個類是否接口類,是否定義爲public類型;是否定義爲abstract類型; 類是否被final修飾等。

#[rustfmt::skip]
#[allow(dead_code)]
pub mod access_flags {
    pub const ACC_PUBLIC:            u16 = 0x0001; //聲明爲public,可以從包外訪問
    pub const ACC_PACC_PRIVATE:      u16 = 0x0002; //聲明爲private,自己能從定義該方法的類中訪問
    pub const ACC_PACC_PROTECTED:    u16 = 0x0004; //聲明爲protected,子類可以訪問
    pub const ACC_PACC_STATIC:       u16 = 0x0008; //聲明爲static
    pub const ACC_PACC_FINAL:        u16 = 0x0010; //聲明爲 final,不能被覆蓋
    pub const ACC_PACC_SYNCHRONIZED: u16 = 0x0020; //聲明爲synchronized,對該方法的調用,將包裝在同步鎖(monitor)裏
    pub const ACC_PACC_BRIDGE:       u16 = 0x0040; //聲明爲bridge方法,由編譯器產生
    pub const ACC_PACC_VARARGS:      u16 = 0x0080; //表示方法帶有邊長參數
    pub const ACC_PACC_NATIVE:       u16 = 0x0100; //聲明爲 native,該方法不是用Java語言實現的
    pub const ACC_PACC_ABSTRACT:     u16 = 0x0400; //聲明爲abstract,該方法沒有實現代碼
    pub const ACC_PACC_STRICT:       u16 = 0x0800; //聲明爲strictfp,使用FP-strict浮點模式
    pub const ACC_PACC_SYNTHETIC:    u16 = 0x10;  //該方法是由編譯器合成的,而不是有源代碼編譯出來的
}

access_flags 一共16個標誌可以使用,其他的屬於符合的標誌 可以通過上面8個計算出來,具體的 如果 我想表達 一個 public interface 只需要 進行或運算 0x0001 | 0x0200 = 0x0201

類索引、父類索引與接口索引集合

類索引、父類索引 都是u2 類型、接口索引集合是多個u2類型的接口名索引的集合。類索引指向常量池中中的索引的一個類的全限定名稱,父類索引 由於Java 中沒有多重索引所以只能有一個父類並且一般 除了 java.lang.Object 外其他類都有父類,一般 普通類都繼承了java.lang.Object,所以類的索引都不爲0 ,接口索引 用來描述 這個類實現了哪些接口 ,順序從左到右 的順序爲索引,如果接口數爲0,那麼後面接口表就沒有實際數據。
在這裏插入圖片描述

字段表集合

字段表(field_info) 用於描述接口或者類中聲明的變量。字段(field)包括類級變量以及實例級變量,在字節碼文件中,每一個field項都對應這一個類或者接口中的字段(變量)信息,用於表示一個字段的完整信息,比如字段的標識符,訪問 修飾符(public、private 或 protected)、是類變量還是實例變量(static 修飾符)、是否是常量(final修飾符)等,值的注意的一點是 由於局部變量並不包含在字段表中 所以 java的字段的作用域都是一樣的 那麼就是說在java中 一個字段(變量) 是無法重載的換句話說 只要變量名字一樣 就是不可以的,不管類型是不是不一樣。然而在字節碼文件中 相同字段名 但是不同修飾符的變量是可以存在的。

/**

字段表結構
access_flags : 訪問標誌  基本8種 總共16種
ACC_PUBLIC 0x0001 | ACC_FINAL 0x0010
ACC_SUPER 0x0020 | ACC_INTERFACE 0x0200
ACC_ABSTRACT 0x0400 | ACC_SYNTHETIC 0x1000
ACC_ANNOTATION 0x2000 | ACC_ENUM 0x4000

name_index : 字段名(變量名)索引 引用常量池一個utf字符串
descriptor_index : 字段描述索引 引用常量池一個utf字符串

attributes_count 當前字段附加屬性的數量。
attributes 屬性表attribute_info,一個字段可以關聯多個屬性表。


*/
#[derive(Clone, Debug)]
pub struct FieldInfo {
    pub access_flags: u16,
    pub name_index: u16,
    pub descriptor_index: u16,
    pub attributes_count: u16,
    pub attributes: Vec<AttributeInfo>,
}

我們 可以看到字段表就是 對字段 做出描述的表,包括了 字段名 、字段的 類型 、字段訪問表示等,關於最後 2個 方法表屬性 後面將會提到。

在這裏插入圖片描述

方法表

在Java 中是不允許存在帶有同一簽名(描述符)並且方法名也相同的,這麼做完全是爲了避免 如果方法簽名(描述符)都一致 編譯器將無法判斷實際要調用哪個方法, 但如果想要 聲明多個具有相同方法名的方法,在面向對象中的多態特性中,方法重載 可以滿足這個特性,只要實現了具有相同方法名的方法但具有不同的方法簽名(描述符)即可。

  • access_flags 類或接口的初始化方法有Java虛擬機隱式調用,類在初始化的時候, 默認調用類的構造器 () 方法,並且() 方法只允許使用 ACC_PUBLIC、ACC_PRIVATE以及ACC_PROTECTED標誌,
  • name_index name_index的值必須是對常量池表的一個有效索引。常量池表在該處索引處的成員必須是CONSTANT_Utf8_info結構,它要麼表示一個特殊的方法的名字( 或) ,要麼表示一個方法的有效非限定名。
  • descriptor_index descriptor_index的值必須是對常量池表的一個有效索引。常量池表示在該索引處的成員必須是CONSTANT_Utf8_info 結構,此結構表示一個有效的方法的描述符。簡單來說 就是 一個索引 可以引用常量池 裏一個字符串 這個字符串 是一個該函數參數和返回值的描述符 。
  • attribute_info 可以關聯多個屬性表
/**
方法表
- access_flags : 訪問標誌
- name_index : 方法名索引,引用常量池一個utf-8方法名字符串
- descriptor_index : 方法描述符索引,引用常量池一個utf-8描述符字符串
- attribute_info : 屬性表
- samples : public static void main(String[] args) {}
          access_flags  0x0001 |
          name_index -> utf8(name_index)  -> main
          descriptor_index -> utf8(descriptor_index)  -> ([Ljava/lang/String;)V 參數 string數組 返回值 Void

*/
pub struct MethodInfo{
    pub access_flags: u16,
    pub name_index: u16,
    pub descriptor_index: u16,
    pub attribute_info:Vec<attributeInfo>,
}

屬性表

屬性(attribute) 表的通用結構

pub struct  AttributeInfo{
    pub attribute_name_index:u16,
    pub Vec<info>,
}
  • attribute_name_index 是一個對當前文件常量池的16位無符號索引,引用了一個CONSTANT_Utf8_info結構,用以表示當前屬性的名字。
  • info 是一個自定義的數據結構,用於存儲屬性的數據信息。
屬性名 class文件 出現位置 JavaSE
ConstantValue field_info 45.3 1.0.2
Code method_info 45.3 1.0.2
Execptions method_info 45.3 1.0.2
SourceFile ClassFile 45.3 1.0.2
LineNumberTable Code 45.3 1.0.2
LocalVariableTable Code 45.3 1.0.2
InnerClasses ClassFile 45.3 1.1
Synthetic ClassFile、field_info 、 method_info 45.3 1.1
Deprecated ClassFile、field_info 、 method_info 45.3 1.1
EnclosingMethod ClassFile 49.0 5.0
Signature ClassFile、field_info 、 method_info 49.0 5.0
SoureceDebugExtension ClassFile 49.0 5.0
LocalVariableTypeTable Code 49.0 5.0
RuntimeVisibleAnntations ClassFile、field_info 、 method_info 49.0 5.0
RuntimeInvisibleAnnotations ClassFile、field_info 、 method_info 49.0 5.0
RuntimeVisibleParameterAnnotations method_info 49.0 5.0
RuntimeInVisibleParameterAnnotations method_info 49.0 5.0
AnnotationDefault method_info 45.3 5.0
StackMapTable Code 50.0 6
BootstrapMethods ClassFile 45.3 7
RuntimeVisibleTypeAnnotations ClassFile、field_info 、 method_info 、 Code 52.0 8
RuntimeInVisibleTypeAnnotations ClassFile、field_info 、 method_info 、 Code 52.0 8
MethodParameters method_info 45.3 8
不同的虛擬機可能會有自己額外的屬性,但是對於一個Java虛擬機能正確解讀Class 文件就以下5個屬性起到了關鍵作用。
  • ConstantValue
  • Cod
  • StackMapTable
  • Exceptions
  • BootstrapMethods
對於Java se平臺的類庫正確解讀class文件其關鍵作用的12個屬性。
  • InnerClasses
  • EnclosingMethod
  • Synthetic
  • Signature
  • RuntimeVisibleAnntations
  • RuntimeInvisibleAnnotations
  • RuntimeVisibleParameterAnnotations
  • RuntimeInVisibleParameterAnnotations
  • RuntimeVisibleTypeAnnotations
  • RuntimeInVisibleTypeAnnotations
  • AnnotationDefault
  • MethodParameters
自定義屬性的規範
  1. 編譯器可以 自定義屬性表裏的屬性,這不影響Class文件的正常執行,所以JVM必須能跳過 不能識別的自定義屬性。
  2. JVM規範明確規定Java虛擬機實現僅僅因爲class文件包含新屬性而拋出異常或以其他形式拒絕使用class文件。
  3. 自定義屬性名不能重複,否則會引起衝突。

Code屬性

code 屬性 會記錄 方法內部的虛擬機指令信息 ,和異常信息表,並且 Code 屬性 內部還可以放其他 屬性表。

  • 位於method_info 中,是爲了識別一個Class 文件基礎的屬性3個屬性之一
  • Code屬性用於存儲方法字節碼指令以及一些其他輔助信息
  • 除了 abstract、native 關鍵字聲明的抽象方法、本地方法 不含COde屬性,其他都含Code屬性。
#[derive(Debug, Clone)]
pub struct CodeAttribute {
    pub attributes_name_index:u16, /** 對常量池表的一個有效索引,常量池表在該索引處的成員閉學式CONSTANT_Utf8_info結構,用以表示字符串"Code" */
    pub attributes_length:u32, /** 給出了當前屬性的長度,不包括初始 6個字節  */
    pub max_stack: u16, /** 給出了當前放大的操作數佔在方法執行的任何時間點的最大深度  */
    pub max_locals: u16, /** 給出了分配在當前方法引用的局部變量表中的局部變量個數,其中也包括調試此方法時用於傳遞參數的局部變量。  */
    pub code_length: u32, /** 給出了當前方法 code[] 數組的字節數。  */
    pub code: *mut Vec<u8>, /** code[] 數組給出了實現當前方法的Java虛擬機代碼的實際字節內容.  */
    pub exception_table_length: u16, /** 給出了 exception_table 表的成員個數  */
    pub exception_table: Vec<Exception>, /** 每個Exception 都是 code[] 數組中的一個異常處理器.  */
    pub attributes_count: u16, /** 給出了Code屬性中attributes[] 成員的個數  */
    pub attributes_info: Vec<AttributeInfo>,  /** 一個AttributeInfo 的結構體,可以放入其他類型的屬性表   */

}

#[derive(Debug, Clone)]
pub struct Exception {
    /**  start_pc 和 end_pc 的值必須是當前code[]中某一指令操作碼的有效索引。 */
    pub start_pc: u16,

    /** end_pc 是 code[] 中某一指令操作碼的有效索引,end_pc 另一種取值是 code_length 的值 ,即code[]的長度. start_pc < end_pc
    當程序計數器 處於 x條指令 處於 start_pc <= x < end_pc  也就是說 2個字節 65535 start_pc 從0 開始 但是 有個設計缺陷 end_pc 最大 也是 65535 但是 < end_pc
    把 end_pc 65535 排除在外了,這樣導致如果 Code 屬性如果長度剛好是 65535個字節 最後一條指令 不能被異常處理器 所處理。
    */
    pub end_pc: u16,

    /** handler_pc項的值表示一個異常處理器的起點。handler_pc 的值必須是同時使對當前code[] 和其中某一指令操作碼的有效索引。簡單來說 就是catch 處理指令的 code號 */
    pub handler_pc: u16,

    /**
      值不爲0,對常量池的一個有效索引。常量池表在該索引處的成員必須是CONSTANT_Class_info 結構,用以表示當前異常處理器需要捕捉的異常類型。
      只有當拋出的異常是制度的類或其自類的實例時,纔會調用異常處理器。 驗證器(verifier) 會檢查這個類是不是 Thorwable 或 Throwable的子類
      如果 catch_type 爲 0,所有異常拋出是都會調用這個異常處理器。
    */
    pub catch_type: u16,

}

在這裏插入圖片描述

在這裏插入圖片描述

ConstantValue屬性

  • Java虛擬機必須實現的3個屬性之一,ConstantValue 屬性是定長屬性,位於field_info結構屬性表中。
  • 一個field_info最多隻能包含一個 ConstantValue屬性,該屬性主要用於通知Java虛擬機對代碼中類變量(不包括實例變量)執行初始化操作。
  • 類變量初始化,兩種方式
    1. (接口)初始化方法()完成
    2. ConstantValue屬性完成,如果一個類變量 final、static 修飾,並且是原始數據(int long double …)類型 或String 類型 這個類能夠被ConstantValue初始化,反之 如果不是 原始數據 、String類型 沒有 被final 修飾的話只能 由 () 方法完成初始化操作。
/**
    定長屬性,位於field_info結構的屬性表中。
*/
#[derive(Debug, Clone)]
pub struct ConstantValue{
    /** 常量池列表中CONSTANT_Utf8_info常量項的有效索引,通過這個索引即可成功獲取當前屬性的簡單名稱, 即"ConstantValue"*/
    pub attribute_name_index:u16,
    /** 固定位2 ,*/
    pub attribute_length:u32,
    /**  指向常量池的CONSTANT_Long、CONSTANT_Float 、CONSTANT_Double 、
    CONSTANT_Integer 、CONSTANT_String 中的一種常量池結構體    */
    pub constantvalue_index:u16,
}

在這裏插入圖片描述

在這裏插入圖片描述

Exceptions屬性

  • 和前面2個屬性一樣,屬於Java 虛擬機必須識別的3個屬性之一,位於method_info項屬性表中。
  • 一個method_info 項屬性表中最多包含一個Exception屬性,用於例舉出當前方法通過關鍵字 thorws 可能拋出的異常信息。
#[derive(Debug, Clone)]
pub struct ExceptionsAttribute{
    /** 常量池列表中CONSTANT_Utf8_info常量項的有效索引,可以獲取當前屬性的簡單名稱,即 字符串 "Exceptions"。 */
    pub attribute_name_index:u16,
    /** 指明瞭Exception屬性值的長度 (不包括 attribute_lenght 和  attribute_name_index)*/
    pub attribute_lenght:u32,
    /** 指明瞭後序exception_index_table[] 項的數組的長度,其中每一個成員必須是一個指向常量池列表中Constant_Class_info */
    pub number_of_exceptions:u16,
    /** 常量項的有效索引通過這個索引值,即可成功獲取當前方法通過thorws 可能拋出的異常信息 */
    pub exception_index_table:Vec<u16>
}

當 throw new Exception(“xxxx!”); 拋出異常時 時 就會產生一個一個關於宜昌的屬性。

在這裏插入圖片描述
在這裏插入圖片描述

LineNumberTable 屬性

在日常生活中,我們經常會根據 日誌文件中給出的 錯誤行號和所屬的文件名來分析和解決錯誤,如果 程序出現錯誤 我們不想 輸入錯誤行號以及錯誤所屬文件名稱,可以在編譯的時候 使用 “-g:none”,使用了這個選項後,堆棧信息中將再也不輸出任何錯誤行號語句錯誤代碼所屬的文件名稱。

  • LineNumberTable 其實就是用來描述 Java代碼中的行號與字節碼文件中的字節碼行號之間的對應關係。
  • LineNumberTable是一個可選屬性,它位於Code_atribute 項的屬性表中。
#[derive(Debug, Clone)]
pub struct LineNumberTable{
    /** 常量池列表中CONSTANT_Utf8_info常量項的有效索引,可以獲取當前屬性的簡單名稱,即 字符串 "LineNumberTable"。 */
    attribute_name_index:u16,
    /** 指明瞭後序2個屬性屬性的長度 (不包括 attribute_name_index attribute_length ) */
    attribute_length:u32,
    /** 指明瞭 line_number_tabel 項數組的長度,*/
    line_number_table_length:u16,
    line_number_info:Vec<LineNumber>,
}

#[derive(Debug, Clone)]
pub struct LineNumber {
    /**表示字節碼文件中的字節碼行號*/
    pub start_pc: u16,
    /**表示Java代碼中的行號*/
    pub line_number: u16,
}

在這裏插入圖片描述

SourceFile 屬性

  • 可選屬性,位於ClassFile的屬性表中,用於記錄生成字節碼文件的源文件名稱。如果在編譯是設定有選項 “-g:none” 時,那麼程序中一旦出現異常時,堆棧信息中將再也不會輸出任何錯誤行號以及錯誤代碼所屬的文件名稱。
#[derive(Debug, Clone)]
pub struct SourceFile_attribute {
    /** 常量池列表中CONSTANT_Utf8_info常量項的有效索引,可以獲取當前屬性的簡單名稱,即 字符串 "SourceFile"。 */
    attribute_name_index:u16,
    /** 值固定位 2*/
    attribute_length:u32,
    /** 常量池列表中CONSTANT_Utf8_info常量項的有效索引,通過這個索引既可以獲取源文件的名稱 */
    sourcefile_index:u16,
}

在這裏插入圖片描述

LocalVariableTable 屬性

  • 可選屬性,用於調試器在執行方法過程中可以用它來確定某個局部變量的值,此屬性描述的是局部變量表中的局部變量與Java代碼中定義變量之間的對應關係。
  • 位於Code_attribute 項的屬性表中
/**
   局部變量表  存放方法的局部變量信息

*/
pub struct InnerClassesAttribute{
    /** 常量池列表中CONSTANT_Utf8_info常量項的有效索引,可以獲取當前屬性的簡單名稱,即  "InnerClasses"。 */
    pub attribute_name_index:u16,
    /** 指明瞭後序2個屬性屬性的長度 (不包括 attribute_name_index attribute_length )  */
    pub attribute_legth:u32,
    /** 指明瞭後序inner_classes[] 項的數組長度,也就是一個類中究竟含有多少個內部類*/
    pub number_of_classe:u16,

    classes:Vec<InnerClassesInfo>,

}

pub struct  InnerClassesInfo{
    /**常量池列表中CONSTANT_Class_info常量項的有效索引,用以表示類或接口*/
    inner_class_info_index:u16,
    /**如果Class不是類或接口的成員(也就是Class爲頂層類或接口)、局部類或匿名類、
    那麼outer_class_info_index項的值爲0,否則這個項的值必須是對象常量池表的一個有效索引,
    常量池表在該索引處的成員必須是Constant_class_INFO結構,代表一個類或接口,Class
    爲這個類或接口的成員*/
    outer_class_info_index:u16,
    /** 如果 C 是匿名類,則爲0 ,否則是對常量池表UTF8結構的一個有效索引,表示由與C的class
    文件相對的源文件所定義的C的原始簡單名稱*/
    inner_name_index:u16,
    /** C的訪問標誌 */
    inner_class_access_flags:u16,
}

在這裏插入圖片描述

InnerClasses 屬性

  • 用於描述內部類 與宿主之間的關聯關係,該屬性位於 ClassFile 的屬性表中。 如果一個類 含有內部類,那麼編譯器就會生成InnerClasses屬性。
pub struct InnerClassesAttribute{
    /** 常量池列表中CONSTANT_Utf8_info常量項的有效索引,可以獲取當前屬性的簡單名稱,即  "InnerClasses"。 */
    pub attribute_name_index:u16,
    /** 指明瞭後序2個屬性屬性的長度 (不包括 attribute_name_index attribute_length )  */
    pub attribute_legth:u32,
    /** 指明瞭後序inner_classes[] 項的數組長度,也就是一個類中究竟含有多少個內部類*/
    pub number_of_classe:u16,

    classes:Vec<InnerClassesInfo>,

}

pub struct  InnerClassesInfo{
    /**常量池列表中CONSTANT_Class_info常量項的有效索引,用以表示類或接口*/
    inner_class_info_index:u16,
    
    outer_class_info_index:u16,
    inner_name_index:u16,
    inner_class_access_flags:u16,
}

在這裏插入圖片描述

to be continu...

參考資料:

— 周志華:《深入理解 Java 虛擬機》
— Tim Lindholm 、Frank Yellin、Gilad Bracha 、Alex Buckley 《 java虛擬機規範 Java Se8》
— 高翔龍《java 虛擬機精講》
— 張秀宏 《自己動手寫 java虛擬機》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章