1. 程式人生 > 程式設計 >【Java雜貨鋪】JVM#Class類結構

【Java雜貨鋪】JVM#Class類結構

程式碼編譯的結果從本地機器碼轉為位元組碼,是儲存格式發展的一小步,卻是程式語言的一大步。——《深入理解Java虛擬機器器》

計算機只認識0和1.所以我們寫的程式語言只有轉義成二進位制本地機器碼才能讓機器認識。然而隨著虛擬機器器的發展,包括Java在內的很多語言,都選擇了一種和作業系統、機器指令集無關的中立儲存格式來儲存編譯後的資料。

無關性

我們都知道Java經典標語,“一次編譯,到處執行”。實現這一目標,每個平臺上定製的虛擬機器器,需要讀取統一的資料。這種資料不依賴於任何一種平臺,甚至不關心是由哪種語言編譯來的,只要統一了格式,虛擬機器器就能正確的使用它。這種統一的格式就是——位元組碼(Class檔案)。

Class檔案中儲存了Java虛擬機器器指令集和符號表以及若干其他輔助和結構化約束。處於安全考慮,Class檔案中使用了許多強制性的語法和結構化約束。

Class類檔案的結構

下面來看下本文的硬菜,Class檔案的結構。雖說大佬書中是以JDK1.4為版本講述的,但是它所包含的指令、屬性是Class檔案中最重要最基礎的。後續不同的版本都是對它的增強。

任何一個Class檔案都對應著唯一一個類或者介面的定義資訊,但是反過來說,類和介面並不一定都得定義在檔案裡(譬如類和介面也可以通過類載入器直接生成)。

Class檔案是以一組以8位位元組為基礎單位的二進位制流,這個資料專案嚴格按照順序緊湊地排列在Class檔案之中,中間沒有新增任何分隔符,這使得整個Class檔案中儲存的內容幾乎是程式執行的必要資料,沒有空格存在。

Class有兩種資料型別(雖然用十六進位制編輯器開啟,看上去都是十六進位制字元):無符號數和表。無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字串值。表是由多個無符號數或者其他表作為資料項構造成的符合資料型別,所有表都習慣性地以“info_”結尾。表用於描述層次關係的複合結構資料,整個Class檔案實質上就是一張表。

Class結構

其中類似於緊挨著的constant_pool_count、constant_pool 這樣的資料可視為一個整體(一個表),前面記錄後者資料的數量。

魔數與Class檔案的版本

看class檔案結構那張表,第一個就是u4 magic。這是一個佔了4個位元組的魔數,它的唯一作用就是確定這個檔案是否為一個Class檔案。它就是一個標誌,告訴虛擬機器器自己是Class檔案,這樣做更加安全,四個位元組儲存的值是固定的,十六進位制下為“0xCAFEBABE”,咖啡寶貝。

接下來分別是兩個位元組的minor(次版本)和兩個位元組的major(主版本)。分別儲存著此Class檔案時何種版本的編譯器編譯的,例如50.3,50就是主版本3就是次版本。在執行時可以向下相容,比如51版本虛擬機器器可以執行50.3版本的class檔案,但是反過來就不行了。

常量池

緊接著 constant_pool_count、constant_pool就是常量池部分。常量池可以理解為Class檔案的資源倉庫,它是Class檔案結構中與其他專案關聯最多的資料型別,也是佔用Class檔案最大的資料專案之一。

首先兩位元組的constant_pool_count是統計後面constant_pool的常量數量的。注意後面的數量是從1開始,例如constant_pool_count儲存的數字是22,那麼constant_pool中就儲存了21個資料項。這麼設計是為了讓“第0個位置”儲存寫特殊的資料。Class檔案只有這一部分計數是從1開始的,其他部分還是從0開始。

常量池中主要儲存兩大類常量:字面量和符號引用。字面量好理解就是注入字串、final修飾的常量值等等。符號引用主要包含一下三個常量:

  1. 類和介面的全限定名
  2. 欄位的名稱和描述符
  3. 方法的名稱和描述符

Class檔案中不會儲存各個方法、欄位的最終記憶體分佈,只有在執行到特定的程式碼時才會知道真正的記憶體入口(某資訊的地址)。在JDK1.4中,常量池可包含的常量項如下(以後的版本會對內容進行擴充):

常量池專案型別

最麻煩的這些型別分別有自己的結構,不過共同的特點是第一個位元組都儲存著tag,即告訴虛擬機器器自己那種常量項。從這部分內容可以看出很多東西,比如說一個變數名稱最大時兩個位元組,即64KB英文字元大小,當然按常理來說不會出現這樣變態的變數名吧。

訪問標誌

在常量池結束之後,緊接著兩個位元組代表訪問標誌(access_flags),這個標誌用於識別一些類或者介面層次的訪問資訊。

訪問標誌使用或來計算,比如一個類被ACC_PIBLIC(0x0001)、ACC_SUPER(0x0020)所修飾,那麼計算為0x0001|0x0020 = 0x0021,該值就是被訪問標誌儲存的值。Java中有專門計算關鍵字的包。

類索引、父類索引與介面索引集合

類索引(this_class)和父類索引(super_class)都是一個u2型別的資料,而介面索引是一組u2型別的資料的集合。Class檔案中有這三項來確定繼承關係。除了Object類以外,所有的夫索引都不是0。如果結構計數器的大小是0,那麼後面那部分就沒有資料。

欄位表集合

欄位表用於描述介面或者類中宣告的變數。欄位包括類級變數和例項級(物件級)變數,但不包括方法內部的區域性變數。以下是欄位表結構和欄位表的第一個屬性訪問標誌。

access_flags 的計算方式和前面類或者介面的訪問表示相同。後面緊跟著兩個屬性是name_index 和 descriptor_index,分別代表著簡易名稱和方法描述符。

欄位表集合中不會列出從超類或者父介面中繼承下來的欄位,但是可以列出本來Java程式碼中不存在的欄位,譬如在內部類中為了保持對外部類的訪問性自動新增的欄位。另外在Java中,同一個類不能出現簡易名稱相同的欄位名,例如int name,後面緊跟著String name。但是在位元組碼層面,簡易名稱可以相同,後面的描述不同就好了。

方法表集合

方法表的結構和欄位表的機構基本類似。

與欄位表集合相對應,如果父類方法在子類中沒有被重寫,方法表集合中就不會出現父類的方法資訊。在Java語言中,要過載一個方法,除了要與原方法具有相同的簡單名稱之外,還要求擁有一個與原方法不同的特徵簽名。特徵簽名就是一個方法中各個引數在常量池中的欄位符號引用的集合,也就是因為返回值不會包含在特徵簽名中,所以僅僅是返回值不同,不是過載。

屬性表集合

在Class檔案、欄位表、方法表都攜帶自己的屬性表集合。屬性表的資料專案相對於其他部分比較寬鬆一點,但是內容也有很多。下面來看一下比較重要的。

Code屬性

Java類的程式方法體中的程式碼經過編譯後儲存在Code屬性中,但是介面和抽象類中的方法就不存在Code屬性中。

max_locals代表了局部變量表所需要的儲存空間,其中最小單位是Slot。其中Slot可以複用,當程式碼執行超出一個區域性變數的作用域時,這個區域性變數所佔的Slot可以被其他區域性變數所使用,極大節省了空間。

code_length和code值儲存的時Java原始碼編譯後生成的位元組碼指令。由於每個code只佔了一個位元組,所以能表示的指令數只有256個。code_length的長度雖然時四個位元組,但是由於虛擬機器器的規定只能使用兩個位元組,所以最大隻能編譯65535條指令,一般來說也是夠用了,但是在編譯複雜的JSP的時候要注意,某些編譯器會把JSP內容和頁面輸出的資訊歸併於一個方法中,就可能導致編譯失敗。

值得一提的是,Javac在編譯方法的時候,引數即使你沒有填,agrs_size也可能是1,這是由於隱式傳進去了this,當然static修飾的方法引數就是0(不填寫的情況下)。

曾經使用try-catch的時候,注意到finlly不會改變區域性變數的值,以為是try已經return了,return之後才去執行的finlly中的資料,其實不然。例如下面這段程式碼。

public int inc(){
    int x;
    try{
        x=1;
        return x;
    }catch(Exception e){
        x=2;
        return x;
    }finally{
        x=3;
    }
}
複製程式碼

這段程式碼永遠不會輸出x=3,執行順序是這樣的(以不會丟擲異常為例):首先執行x=1,此時區域性變數等於1.然後讀到return指令,然後將x的值賦給一個空間,這個空間是return時返回的值,我們暫且將這塊空間起個名字,叫做returnX,然後程式碼進入finally,注意此時,還在這個inc()方法的作用域中。然後將x賦值等於3,最後執行return指令,返回剛才那塊returnX空間的值給呼叫者。離開inc()作用域,此時x那塊Slot可以被複用。

其他

  1. Exceptions 儲存方法throws後面的異常。
  2. LineNumberTable 不是必填項,但是預設填上,如果不填,拋異常棧的時候就無法定位到哪一行了。
  3. LocalVariableTable 不是必填項,用於描述棧幀中區域性變量表中的變數於Java原始碼中定義的變數之間的關係。
  4. SourceFile 記錄生成這個Class檔案原始碼的名稱
  5. ConstantValue 通知虛擬機器器自動為靜態變數賦值,在初始化之前就進行賦值。
  6. InnerClasser 記錄內部類和宿主類之間的關聯。
  7. Signature 這個寫AOP的時候經常見,此屬性會為泛型記錄資訊,因為Java在編譯的時候會進行泛型擦除,所以需要記錄一下,讓Java在執行的時候可以拿到泛型的原始資訊。
  8. BootstrapMethods 這個屬性儲存invokedynamic指令引用的引導方法限定符,和Invoke包有很大關係。

位元組碼指令

位元組碼指令不會超過256個,一般來說一個指令後面會跟著引數,這很自然,就像我們寫方法時需要加入引數(沒有引數也是種引數)。但是由於Java虛擬機器器採用面向運算元棧而不是暫存器(編譯語言)的架構,所以大多數情況下只包含一個操作碼。

由於位元組碼數量有限,所以很多指令會被強制統一。比如處理boolean、byte、short和char型別的陣列時,也會轉化為對應的int型別的位元組碼指令來處理。

位元組碼操作的時候可能會導致溢位,例如兩個很大的正整數相加,結果可能會稱為一個負數。當一個操作產生溢位時,將會使用有符號的無窮大來表示,如果某個操作結果沒有明確的數字定義的話,將會使用NaN值來表示。所有使用NaN值作為運算元的算術操作,結果會返回NaN。

Java虛擬機器器可以支援方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都時使用管程(Monitor)來支援的。可以看作Synchronized此時拿的鎖就是Monitor。