JVM-類文件結構

 

目錄

 

一.概述

二.Class類文件的結構

2.1 魔數

2.2 Class文件的版本

2.3 常量池

2.4 訪問標誌

2.5 類索引,父類索引與接口索引集合

2.6 字段表集合

2.7 方法表集合

2.8 屬性表集合

 


一.概述

在Java中,JVM可以理解的代碼就叫做字節碼(即擴展名爲.class的文件),它不面向任何特定的處理器,只面向虛擬機。Java語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以Java程序運行時比較高效,而且,由於字節碼並不針對一種特定的機器,因此,Java程序無須重新編譯便可在多種不同的操作系統的計算機上運行。

實現語言無關性的基礎仍然是虛擬機和字節碼存儲格式。Java虛擬機不和包括Java在內的任何語言綁定,它只與“Class文件”這種特定的二進制文件格式所關聯,Class文件中包含了Java虛擬機指令集和符號表以及若干輔助信息。基於安全方面的考慮,Java虛擬機規範要求在Class文件中使用許多強制性的語法和結構化約束,但任何一門功能性語言都可以表示爲一個能被Java虛擬機所接受的有效的Class文件。

Clojure(Lisp 語言的一種方言)、Groovy、Scala 等語言都是運行在 Java 虛擬機之上。下圖展示了不同的語言被不同的編譯器編譯成.class文件最終運行在 Java 虛擬機之上。.class文件的二進制格式可以使用WinHex查看。

javaèææº

可以說.class文件是不同的語言在 Java 虛擬機之間的重要橋樑,同時也是支持 Java 跨平臺很重要的一個原因。

二.Class類文件的結構

Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數據,沒有空隙存在。當遇到需要佔用8位字節以上空間的數據項時,則會按照高位在前(最高位字節在地址最低位,最低位字節在地址最高位)的方式分隔若干個8位字節進行存儲。

任何一個Class文件都對應着唯一一個類或接口的定義信息,但反過來說,類或接口並不不一定都得定義在文件裏(譬如類或接口也可以通過類加載器直接生成)。下面討論只是通俗地將任意一個有效的類或接口所應當滿足的格式稱爲“Class文件格式”,實際上它並不一定以磁盤文件的形式存在。

根據Java虛擬機規範的規定,Class文件格式採用一種類似C語言結構體的僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表。

無符號數屬於基本的數據類型,以u1,u2,u4,u8分別代表1個字節,2個字節,4個字節和8個字節的無符號數,無符號數可以用來描述數字,索引引用,數量值或者按照UTF-8編碼構成的字符串。

表是由多個無符號數或者其他表作爲數據項構成的符合數據類型,所有表都習慣性以“_info”結尾,表用來描述有層次關係的複合結構的數據,整個Class文件本質上就是一張表。由下面的數據項構成:

ClassFile {
    u4             magic; //Class 文件的標誌
    u2             minor_version;//Class 的小版本號
    u2             major_version;//Class 的大版本號
    u2             constant_pool_count;//常量池的數量
    cp_info        constant_pool[constant_pool_count-1];//常量池
    u2             access_flags;//Class 的訪問標記
    u2             this_class;//當前類
    u2             super_class;//父類
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一個類可以實現多個接口
    u2             fields_count;//Class 文件的字段屬性
    field_info     fields[fields_count];//一個類會可以有個字段
    u2             methods_count;//Class 文件的方法數量
    method_info    methods[methods_count];//一個類可以有個多個方法
    u2             attributes_count;//此類的屬性表中的屬性數
    attribute_info attributes[attributes_count];//屬性表集合
}

Class文件字節碼結構組織示意圖:

ç±»æ件å­èç ç»æç»ç»ç¤ºæå¾

無論是順序還是數量,甚至於數據存儲的字節序這樣的細節,都是被嚴格規定的,哪個字節代表什麼含義,長度是多少,先後順序如何,都不允許改變。

2.1 魔數

  u4             magic; //Class 文件的標誌

每個Class文件的頭4個字節稱爲魔數(Magic Number),它唯一作用是確定這個文件是否爲一個能被虛擬機接受的Class文件。很多文件存儲標準中都使用魔數進行身份識別。程序設計者很多時候都喜歡用一些特殊的數字表示固定的文件類型或者其它特殊的含義。

2.2 Class文件的版本

    u2             minor_version;//Class 的小版本號
    u2             major_version;//Class 的大版本號

緊接着魔數的4個字節存儲的是Class文件的版本號:第5個和第6個字節是次版本號(Minor Version),第7個和第8個字節是主版本號(Major Version)。高版本的JDK能向下兼容以前版本的Class文件,但不能運行以後版本的Class文件,即使文件格式並未發生任何變化,虛擬機也必須拒絕執行其他版本號的Class文件。

2.3 常量池

    u2             constant_pool_count;//常量池的數量
    cp_info        constant_pool[constant_pool_count-1];//常量池

緊接着主次版本號之後的是常量池入口,常量池可以理解爲Class文件之中的資源倉庫,它是Class文件結構中與其他項目關聯最多的數據類型,也是佔用Class文件空間的最大的數據項目之一,同時它還是在Class文件中第一個出現表類型數據項目。

由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2類型的數據,代表常量池容量計數值(constant_pool_count)。這個容量計數是從1而不是0開始的,將第0位常量空出來是由特殊考慮的,這樣做的目的在於滿足後面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的含義。

常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic Reference)。字面量比較接近Java語言層面的常量概念,如文本字符串,聲明爲final的常量值等。而符號引用則屬於編譯原理方面的概念,包括下面三種常量:

  • 類和接口的全限定名(Fully Qualified Name)
  • 字段的名稱和描述符(Descriptor)
  • 方法的名稱和描述符

Java代碼在進行Javac編譯的時候,並不像C和C++那樣有“連接”這一步驟,而是在虛擬機加載Class文件的時候進行動態連接。也就是說,在Class文件中不會保存各個方法,字段的最終內存佈局信息,因此這些字段,方法的符號引用不經過運行期轉換的話無法得到真正的內存入口地址,也就是無法直接被虛擬機使用。當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析,翻譯到具體的內存地址之中。

常量池中的每一個常量都是一個表,這些表都有一個共同的特點,就是表開始的第一位是一個u1類型的標誌位,代表當前這個常量屬於哪種常量類型。

類型 標誌(tag) 描述
CONSTANT_utf8_info 1 UTF-8編碼的字符串
CONSTANT_Integer_info 3 整形字面量
CONSTANT_Float_info 4 浮點型字面量
CONSTANT_Long_info 長整型字面量
CONSTANT_Double_info 雙精度浮點型字面量
CONSTANT_Class_info 類或接口的符號引用
CONSTANT_String_info 字符串類型字面量
CONSTANT_Fieldref_info 字段的符號引用
CONSTANT_Methodref_info 10 類中方法的符號引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符號引用
CONSTANT_NameAndType_info 12 字段或方法的符號引用
CONSTANT_MothodType_info 16 標誌方法類型
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_InvokeDynamic_info 18 表示一個動態方法調用點

2.4 訪問標誌

在常量池結束之後,緊跟着的兩個字節代表訪問標誌(access_flags),這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個Class是類還是接口;是否定義爲public類型;是否定義爲abstract類型等。具體的標誌位以及標誌的含義如下:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 是否爲public類型
ACC_FINAL 0x0010 是否被聲明爲final,只有類可以設置
ACC_SUPER 0x0020 是否允許使用invokespecial字節碼指令的新語意,invokespecial指令的語意在JDK1.0.2發生過改變,爲了區別這條指令使用哪種語意,JDK1.0.2之後編譯出來的類這兩個標誌都必須爲真。
ACC_INTERFACE 0x0200 標識這是一個接口
ACC_ABSTARCT 0x0400 是否爲abstract類型,對於接口或者抽象類來說,此標誌值爲真,其他類值爲假
ACC_SYNTHETIC 0x1000 標識這個類並非由用戶代碼產生的
ACC_ANNOTATION 0x2000 標識這是一個註解
ACC_ENUM 0x4000 標識這是一個枚舉

acc_flags中一共有16個標誌位可以使用,當前只定義了其中8個,沒有使用到的標誌位要求一律爲0。

2.5 類索引,父類索引與接口索引集合

    u2             this_class;//當前類
    u2             super_class;//父類
    u2             interfaces_count;//接口
    u2             interfaces[interfaces_count];//一個雷可以實現多個接口

類索引(this_class)和父類索引(super_class)都是一個u2類型的數據,而接口索引集合是一組u2類型的數據的集合,Class文件中由這三項數據來確定這個類的繼承關係。類索引用來確定這個類的全限定名,父類索引於確定這個類的父類的全限定名。由於Java語言不允許多重繼承,所以父類索引只有一個,除了java.lang.Object之外,所以的Java類都有父類,因此除了java.lang.Object之外,所以的Java類的父類索引都不爲0.接口索引集合就用來描述這個類實現了哪些接口,這些被實現的接口將按implements語句(如果這個類本身是一個接口,則應當是extends語句)後的接口順序從左向右排列在接口索引集合中。

類索引,父類索引和接口索引集合都按順序排列在訪問標誌之後,類索引和父類索引用兩個u2類型的索引值表示,它們各自指向一個CONSTANT_Class_info的類型描述符常量,通過CONSTANT_Class_info類型的常量中的索引值就可以找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字符串。

對於接口索引集合,入口中的第一項-u2類型的數據爲接口計數器,表示索引表的容量。如果該類沒有實現任何接口,則該計數器爲0,後面的索引表不再佔用任何字節。

2.6 字段表集合

     u2             fields_count;//Class 文件的字段的個數
     field_info     fields[fields_count];//一個類會可以有個字段

字段表(filed_info)用於描述接口或者類中的聲明的變量。字段包括類級變量以及實例級變量,但不包括方法內部聲明的局部變量。

field info(字段表) 的結構:

å­æ®µè¡¨çç»æ

  • access_flags:字段的作用域(public,private,protected修飾符),是實例變量還是類變量(static修飾符),是否被序列化(transient修飾符),可變性(final),可見性(volatile修飾符,是否強制從主內存讀寫)。
  • name_index:對常量池的引用,表示字段的名稱。
  • descriptor_index:對常量池的引用,表示字段和方法的描述符。
  • attributes_count:一個字段還會擁有一些額外的屬性,attributes_count存放屬性的個數。
  • attributes[attributes_count]:存放具體屬性具體內容。

上述這些信息中,各個修飾符都是布爾值,要麼有某個修飾符,要麼沒有,很適合使用標誌位來表示。而字段叫什麼名字、字段被定義爲什麼數據類型這些都是無法固定的,只能引用常量池中常量來描述。

字段access_flags的取值:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 字段是否public
ACC_FINAL 0x0010 字段是否final
ACC_PRIVATE 0x0002 字段是否private
ACC_PROTECTED 0x0004 字段是否protected
ACC_STATIC 0x0008 字段是否static
ACC_VOLATILE 0x0040 字段是否volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_ENUM 0x4000 字段是否enum
ACC_SYNTHETIC 0x1000 字段是否由編譯器自動產生的

細節:

descriptor_index描述符的作用是用來描述字段的數據類型,方法的參數列表(包括數量,類型以及順序)和返回值。根據描述符規則,基本數據類型(byte,char,double,float,int,long,short,boolean)以及代表無返回值的void類型都用一個大寫字符來表示,而對象類型則用字符L加對象的全限定名錶示。對於數組類型,每一維度將使用一個前置的“[”字符來描述。用描述符描述方法時,按照先參數列表,後返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號“()”之內。

字段表集合中不會列出從超類或者父接口繼承而來的字段,但有可能列出原本Java代碼之中不存在的字段,譬如在內部類中爲了保持對外部類的訪問性,會自動添加指向外部類實例的字段。另外,在Java語言中字段是無法重載的,兩個字段的數據類型,修飾符不管是否相同,都必須使用不一樣的名稱,但是對於字節碼來說,如果兩個字段的描述符不一致,字段重名就是合法的。

2.7 方法表集合

    u2             methods_count;//Class 文件的方法的數量
    method_info    methods[methods_count];//一個類可以有個多個方法

方法表的結構依次包含了訪問標誌,名稱索引,描述符索引,屬性表集合幾項。

æ¹æ³è¡¨çç»æ

方法表中的access_flags取值:

標誌名稱 標誌值 含義
ACC_PUBLIC 0x0001 方法是否爲public
ACC_FINAL 0x0010 方法是否爲final
ACC_PRIVATE 0x0002 方法是否爲private
ACC_PROTECTED 0x0004 方法是否爲protected
ACC_STATIC 0x0008 方法是否爲static
ACC_SYNCHRONIZED 0x0020 方法是否爲synchronized
ACC_BRIDGE 0x0040 方法是否由編譯器產生的橋接方法
ACC_VARARGS 0x0080 方法是否接受不定參數
ACC_NATIVE 0x0100 方法是否爲native
ACC_ABSTRACT 0x0400 方法是否爲abstract
ACC_STRICFP 0x0800 方法是否爲strictfp
ACC_SYNTHETIC 0x1000 方法是否由編譯器自動產生的

與字段集合對應的,如果父類方法在子類中沒有被重寫,方法表集合中就不會出現來自父類的方法信息。但同樣的,有可能出現由編譯器自動添加的方法。

在Java語言中,要重載一個方法,除了要與原方法具有相同的簡單名稱之外,還要求必須擁有一個與原方法不同的特徵前面,特徵簽名就是一個方法中各個參數在常量池中的字段符號引用的集合,也就是因爲返回值不會包含在特徵簽名中,因此Java語言裏面是無法僅僅依靠返回值的不同來對一個已有方法進行重載的。但是在Class文件格式中,特徵簽名的範圍更大一些,只要描述符不是完全一致的兩個方法也可以共存。也就是說,如果兩個方法具有相同的名稱和特徵簽名,但返回值不同,那麼也是可以合法共存同一個Class文件中的。

2.8 屬性表集合

   u2             attributes_count;//此類的屬性表中的屬性數
   attribute_info attributes[attributes_count];//屬性表集合

在Class文件,字段表,方法表都可以攜帶自己的屬性表集合,以用來描述某些場景專有信息。屬性表集合的限制稍微寬鬆了一些,不再要求各個屬性表有嚴格順序,並且只要不與已有屬性名重複,任何人實現的編譯器都可以向屬性表中寫入自己定義的屬性信息,Java虛擬機運行時會忽略掉它不認識的屬性。

 

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