谈谈Java虚拟机——Class文件结构
大家都知道,Java之所以如此受人喜歡,很大的原因是要規于它的跨平臺性。“一次編寫,到處運行”,Java誕生之時曾提出的著名的宣傳口號,充分表達了軟件開發人員對沖破平臺界限的渴求。
或許大部分程序員都認為Java虛擬機執行Java程序是一件理所當然和天經地義的事,但時至今日,商業機構和開源機構已經在Java語言之外發展出一大批在Java虛擬機之上運行的語言,如Clojure、Groovy、JRuby、Jython、Scale等。使用Java編譯器可以把Java代碼編譯為存儲字節碼的Class文件,使用JRuby等其它語言的編譯器一樣可以把程序代碼編譯成Class文件,Java之所以能夠跨平臺運行,是因為Java虛擬機可以載入和執行同一種平臺無關的字節碼。也就是說,實現語言平臺無關性的基礎是虛擬機和字節碼存儲格式,虛擬機并不關心Class的來源是什么語言,只要它符合Class文件應有的結構就可以在Java虛擬機中運行。
?
Class類文件的結構?
?Class文件是一組以8位字節為基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部都是程序運行的必要數據,沒有空隙存在。當遇到需要占用8位字節以上的空間的數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。
根據Java虛擬機規范的規定,Class文件格式采用一種類似于C語言結構體的偽結構來存儲,這種偽結構中只有兩種數據類型:無符號數和表。
無符號數屬于基本的數據類型,以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節、8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值,或者按照UTF-8編碼構成字符串值。
表是由多個無符號數或者其它表作為數據項構成的復合數據類型,所有表都習慣性地以"_info"結尾。表用于描述有層次關系的復合結構的數據,整個Class文件本質上就是一張表。?它由下表所示的數據項構成。
接下來我們根據表中的數據項來描述class文件的格式。
? ? 1.?魔數?
每個Class文件的頭4個字節稱為魔數(Magic Number),它的唯一作用是用于確定這個文件是否為一個能被虛擬機接受的Class文件。很多文件存儲標準中都使用魔數來進行身份識別,譬如圖片格式,如gif或jpeg等在文件頭中都存有魔數。Class文件魔數的值為0xCAFEBABE。如果一個文件不是以0xCAFEBABE開頭,那它就肯定不是Java class文件。? ? 2.?版本號?
? ? 緊接著魔數的4個字節存儲的是Class文件的版本號:第5和第6是次版本號(Minior Version),第7個和第8個字節是主版本號(Major Version)。Java的版本號是人45開始的,JDK1.1之后的每個JDK大版本發布主版本號向上加1,高版本的JDK能向下兼容以前版本的Class文件,但不能運行以后版本的Class文件,即使文件格式并未發生變化。JDK1.1能支持版本號為45.0~45.65535的Class文件,JDK1.2則能支持45.0~46.65535的Class文件。JDK1.7可生成的Class文件主版本號的最大值為51.0。?
? ???
? ? ?3.常量池?
? ? ?緊接著魔數與版本號之后的是常量池入口,常量池是Class文件結構中與其它項目關聯最多的數據類型,也是占用Class文件空間最大的數據項目之一,同時它還是在文件中第一個出現的表類型數據項目。由于常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2類型的數據,代表常量池容量計數值(constant_pool_count)。從1開始計數。第0項騰出來滿足后面某些指向常量池的索引值的數據在特定情況下需要表達"不引用任何一個常量池項目"的意思,這種情況就可以把索引值置為0來表示。但盡管constant_pool列表中沒有索引值為0的入口,缺失的這一入口也被constant_pool_count計數在內。例如,當constant_pool中有14項,constant_poo_count的值為15。Class文件結構中只有常量池的容量計數是從1開始的,對于其他集合類型,包括接口索引集合、字段表集合、方法表集合等的容量計數都是從0開始的。
? ? ?常量池之中主要存放兩大類常量:字面量和符號引用。字面量比較接近于Java語言層面的常量概念,如文本字符串、被聲明為final的常量值等。而符號引用則屬于編譯原理方面的概念,包括了下面三類常量:
- ? ? ?類和接口的全限定名
 - ? ? ?字段的名稱和描述符
 - ? ? ?方法的名稱和描述符 ?
 
? ? Java代碼在進行Java編譯的時候,并不像C和C++那樣有"連接"這一步驟,而是在虛擬機加載Class文件的時候進行動態連接。也就是說,在Class文件中不會保存各個方法和字段的最終內存布局信息,因此這些字段和方法的符號引用不經過轉換的話是無法被虛擬機使用的。當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析并翻譯到具體的內存地址之中。?
? ? 常量池中的每一項常量都是一個表,共有11種結構各不相同的表結構數據,這11種表都有一個共同的特點,就是表開始的第一位是一個u1類型的標志位(tag,取值為1至12,缺少標志為2的數據類型),代有當前對象屬于哪種常量類型,11常量類型所代表的具體含義如下表所示。
? ??
? ? 說了這么多,恐怕還是對常量池有點迷惑吧,我們舉個例子來看一下
? ? 假如我們得到的Class文件的十六進制數的一段序列為:
? ???
? ? 第9位 16轉換為十進制為22,代表常量池中有21個常量。第10位的07帶表的是一個常量的tag值,可以從上表中看到,07代表CONSTANT_Class_info類型,從上表中可以看出,
CONSTANT_Class_info類型的結構有一個u1類型的tag,有一個u2類型的name_index,數量都是1。那么可以看出接下來的第11位與第12位的值0002就是name_index的值。即指向的常量池中的第一個常量。第二項常量的標志位為0x01(看第13位),也就是CONSTANT_Utf_info類型, CONSTANT_Utf_info類型有u2型的length與u1型的bytes。依此類推。? ?如上所述,虛擬機加載Class文件的時候,就是這樣從常量池中得到相對應的數值。
? ?4.訪問標志?
? ?緊接常量池后的兩個字節稱為access_flags,它展示了文件中定義的類或接口的幾段信息。例如,訪問標志指明文件中定義的是類還是接口;訪問標志還定義的在類或接口的聲明中,使用了哪種修飾符oder和接口是抽象的,還是公共的;類的類型可以為final,而final類不可能是抽象的;接口不能為final類型的。這些標志位的定義如下表所示:
? ??
? ? 如一個TestClass類被public關鍵字修飾但沒有被聲明為final和abstract,并且它使用了JDK1.2之后的編譯器進行編譯,因此它的ACC_PUBLIC、ACC_SUPER標志應該為真。因此它的access_flags的值應為:0x0001|0x0020 = 0x0021。
? ? 5.?類索引
? ? 訪問標志后面接下來的兩個字節是類索引(this_class),它是一個對常量池的索引。在this_class位置的常量池入口必須為CONSTANT_Class_info表。該表由兩個部分組成——tag和name_index。tag部分是代表其的標志位,name_index位置的常量池入口為一個包含了類或接口全限定名的CONSTANT_Utf8_info表。?
? ? 6.父類索引
? ? 在class文件中,緊接在this_class之后是super_class項,它是一個兩個字節的常量池索引。在super_class位置的常量池入口是一個指向該類超類全限定名的CONSTANT_Class_info入口。因為Java程序中所有對象的基類都是java.lang.Object類,除了Object類以外,常量池索引super_class對于所有的類均有效。對于Object類,super_class的值為0。對于接口,在常量池入口super_class位置的項為java.lang.Object
? ?7.interfaces_count和interfaces
? ?緊接著super_class的是interfaces_count,此項的含義為:在文件中出該類直接實現或者由接口所擴展的父接口的數量。在這個計數的后面,是名為interfaces的數組,它包含了對每個由該類或者接口直接實現的父接口的常量池索引。每個父接口都使用一個常量池中的CONSTANT_Class_info入口來描述,該CONSTANT_Class_info入口指向接口的全限定名。這個數組只容納那些直接出現在類聲明的implements子句或者接口聲明的extends子句中的父接口。超類按照在implements子句和extends子句中出現的順序在這個數組中顯現。?
? ?8.?fields_count和fields
? ?在class文件中,緊接在interfaces后面的是對在該類或者接口中所聲明的字段的描述。首先是名為fields_count的計數,它是類變量和實例變量的字段的數量總和。在這個計數后面的是不同長度的field_info表的序列(fields_count指出了序列中有多少個field_info表)。只有在文件中由類或者接口聲明了的字段才能在fields列表中列出。在fields列表中,不列出從超類或者父接口繼承而來的字段。另一方面,fields列表可能會包含在對應的Java源文件中沒有敘述的字段,這是因為Java編譯器可以會在編譯時向類或者接口添加字段。
? ?在Java中,描述字段的信息有:字段的作用域、是實例變量還是類變量(static)、可變性(final)、并發可見性(volatile)、可否序列化(trasient)、字段數據類型、字段名稱。這些信息中,各個修飾符都是布爾值,要么有某個修飾符,要么沒有,很適合使用標志位來表示。而字段叫什么名字、字段被定義為什么數據類型,這些都是無法固定的,只能引用常量池中的常量來描述。
? ?
? ?字段修飾符放在access_flags項目中,它與類中的access_flags項目是非常相似的,都是一個u2的數據類型,其中可以設置 的標志位和含義如下表所示
? ?
? ?跟隨access_flags標志的是兩項索引值:name_index和descriptor_index。它們都是對常量池的引用,分別代表著字段的簡單名稱及字段和方法的描述符。描述符的作用是用來描述字段的數據類型、方法的參數列表(包括數量、類型以及順序)和返回值。根據描述符規則,基本數據類型(byte、char、double、float、int、long、short、boolean)及代表無返回值的void類型都用一個大寫字符來表示,而對象類型則用字符L加對象的全限定名來表示。
? ??
? ?對于數組類型,每一個維度將使用一個前置的"["字符來描述,如一個定義的"java.lang.String[][]"類型的二維數組,將被記錄為:"[[Ljava/lang/String;",一個整型數組"int[]"將被記錄為"[I"
? ?用描述符來描述方法時,按照先參數列表,后返回值的順序描述,參數列表按照參數的嚴格順序放在一組小括號"()"之內。如方法void inc()的描述符為"()V",方法java.lang.String.toString()的描述符為"()Ljava/lang/String;"。
? ? 9.method_count和methods
? ? 緊接著field后面的是對在該類或者接口中所聲明的方法的描述。其結構與fields一樣,不一樣的是訪問標志。
? ??
? ? 10.attributes_count和attributes
? ? class文件中最后的部分是屬性,它給出了在該文件類或者接口所定義的屬性的基本信息。屬性部分由attributes_count開始,attributes_count是指出現在后續attributes列表的attribute_info表的數量總和。每個attribute_info的第一項是指向常量池中CONSTANT_Utf8_info表的引引,該表給出了屬性的名稱。
? ? 屬性有許多種。Java虛擬機規范定義了幾種屬性,但任何人都可以創建他們自己的屬性種類,并且把它們置于class文件中,Java虛擬機實現必須忽略任何不能識別的屬性。
? ? java虛擬機預設的9項虛擬機應當能識別的屬性如下表所示。
? ???
轉載于:https://www.cnblogs.com/madonion/articles/2270072.html
總結
以上是生活随笔為你收集整理的谈谈Java虚拟机——Class文件结构的全部內容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: 从一个基础Javascript面试题谈起
 - 下一篇: ASP.NET----实现点击按钮或链接