Java虛擬機詳解(九)------類文件結構


  我們知道計算機是由晶體管、電路板等組裝而成的電子設備,而這些電子設備其實只能識別0與1的信號。

  那么問題來了,我們在操作系統上編寫的Java代碼(由字母、數字等各種符號組成),打包后部署到服務器上,是如何被計算機所識別並運行的呢?另外,操作系統有很多種,包括Windows系統,Linux系統,Mac OS系統等,而我們同樣的Java代碼,卻可以不做任何處理在不同的系統上正常運行,這又是為啥呢?

  帶着這些疑問,你將會在下面的介紹中得到答案!!!

1、Java虛擬機的兩個特性

  在此系列博客第一篇文章中,我們介紹到Java虛擬機的兩個特性。

①、語言無關性

  對於Java語言,我們通過編輯器編寫的Java代碼,后綴一般是.java。通過javac編譯器編譯后,會變成.class結尾的字節碼文件,只有編譯后的.class文件,才能在Java虛擬機上運行。(解壓部署在服務器上的jar包,全是編譯后的class文件)

  再比如對於 JRuby 語言,通過編輯器編寫的代碼后綴是.rb。通過jrubyc 編譯器編譯后,也會變成 .class 結尾的字節碼文件,然后也能在Java虛擬機上運行。

  在比如已正式成為Android官方支持開發語言的Kotlin。也可以編譯成.class字節碼文件,然后在虛擬機上運行。

  我們可以用下面這幅圖來表示:

  

 

   也就是說,不管你是什么語言,只要能通過某種手段生成合乎規范的.class字節碼文件,其實就可以在Java虛擬機上運行,這就是語言無關性。

②、平台無關性

  Write once, run everywhere(一次編寫,到處運行)這是Java語言誕生之處就宣傳的一個口號。Java語言之所以能夠跨平台運行,其實就是因為Java虛擬機對各個平台的適配,在不同的系統下安裝不同的Java虛擬機,我們程序當然能夠在不同的系統上運行。

  

 

   對於文章開頭提出的問題,同樣的程序能夠在不同的系統上正常運行的原因,就是因為我們在不同的系統上安裝了不同的Java虛擬機。

2、class 字節碼文件介紹

  搞清楚了Java代碼的跨平台原理,我們接着來介紹為什么編寫的Java代碼能夠被計算機所識別。

①、字節碼文件

  這其實是上面所說的語言無關性這個特性重要文件——class字節碼文件的功勞。

  Java所有的指令大概有 200 個左右,一個字節(8位)可以存儲 256 種不同的信息,我們將一個這樣的字節稱為字節碼(ByteCode)。

  而 class 文件便是一組以 8 位字節為基礎單位流的二進制流,各個數據項目嚴格按照順序緊湊地排列在 class 文件之中,中間沒有添加任何分隔符,所以整個class 文件中存儲的內容幾乎都是程序運行的必要數據,沒有任何冗余。當遇到需要占用 8 位字節以上空間的數據項時,則會按照高位在前的方式分割成若干個 8 位字節進行存儲。

  比如,對於如下這段代碼:

 1 /**
 2  * Create by YSOcean
 3  */
 4 public class ClassTest {
 5     private static int i = 0;
 6 
 7     public static void main(String[] args) {
 8         System.out.println(i);
 9     }
10 }

  我們將生成的class 文件,通過十六進制編輯器打開(在IDEA中,可以下載HexView插件,安裝完成后,選擇這個class文件,右鍵 HexView)

  

 

   打開后的文件如下:(下面的介紹也都是以這張圖為例)

  

   下面我們會介紹這些十六進制分別代表什么意思。

②、javap 命令

  另外,為了更好的查看 Class 文件字節碼結構,JDK 還為我們提供了一個命令行工具 javap。使用語法如下:

javap <options> <classes>

  通過 javap -help 命令,可以查看相關參數作用:

  

   我們將 ClassTest.class 文件,通過 javap -v ClassTest.class 命令,執行后如下:

  

   這些內容下面也會詳細介紹。

3、無符號數和表

  在介紹這些十六進制之前,我們先介紹 Class 文件的數據類型。

  Class 文件采用一種類似於 C 語言結構體的偽結構來存儲,這種偽結構只有兩種數據類型:無符號數和表

①、無符號數

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

②、表

  表是由多個無符號數或其它表作為數據項所構成的復合數據類型,所有表都習慣行的以“_info”結尾。表用於描述有層次關系的復合結構數據。

  整個 Class 文件本質上就是一張表,結構如下:

  

 

   PS:需要說明的是,由於 Class 文件結構沒有任何分隔符,所以無論是每個數據項的的順序還是數量,都是嚴格限定的,哪個字節代表什么含義,長度多少,先后順序如何,都是不允許改變的。

  下面,我們就來分別介紹這些數據項代表什么含義。  

4、魔數

  每個 class 文件的頭 4 個字節稱為魔數(Magic Number),它的唯一作用是:標識該文件是一個Java類文件。如果沒有識別到該標志,則說明該文件不是Java類文件或者文件已受損。

  由上圖,我們可以看到前 4 個字節是 cafe babe。這是 Gosling 定義的一個魔法數,意思是 Coffee Baby。

  其實很多文件存儲標准中都使用魔數進行身份識別,比如圖片gif或者jpeg,使用魔數而不是使用擴展名來進行識別主要是基於安全考慮,因為文件擴展名可以任意的改動。

5、Class 文件的版本號

  緊隨魔數的 4 個字節存儲的是 class 文件的版本號:第 5 和第 6 個字節是次版本號(Minor Version),第 7 和第 8 個字節是主版本號(Major Version)。

  Java的版本號是從 45 開始的,JDK1.1 之后的每個 JDK 大版本發布主版本號向上加1(JDK1.0~JDK1.1使用了45.0~45.3的版本號),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能運行以后版本的 Class 文件,即使文件格式未發生變化。

  上圖第5、6、7、8個字節為 00 00 00 34。其十進制值為 52,是JDK8的內部版本號。

6、常量池

  緊隨主版本號的是常量池入口,是class文件中第一個出現的表類型數據項目,也是占用Class文件空間最大的項目之一,更是Class文件結構中與其它項目關聯最多的數據類型。

①、常量池容量計數值

  因為常量池中常量的數量是不固定的,所以在常量池的入口要放置一項 u2 類型的數據,代表常量池容量計數值(constant_pool_count)。

  PS:注意,常量池容量計數值是從 1 開始的,而不是從 0 開始。將 0 空出來,是為了滿足后面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的意思。

  Class 文件結構中,只有常量池的容量是從 1 開始的,其它的集合類型,都是從 0 開始的。

  看上圖的十六進制文件,常量池容量計數值為:0x0025,即十進制 37。這就表示常量池中有 36 項常量,索引值分別為 1~36(通過上面javap命令生成字節碼文件可以很明顯看出來有36個)

  

②、常量池內容

  常量池主要存放兩大類常量:

  1、字面量(Literal):字面量比較接近於 Java 語言層面的常量概念,比如 文本字符串、被聲明為 final 的常量值等。

  2、符號引用(Symbolic References):符號引用屬於編譯原理方面的概念,包括下面三類常量:

    類和接口的權限定名(Fully Qualified Name)

    字段的名稱和描述符(Descriptor)

    方法的名稱和描述符。

  需要說明的是,Java代碼在進行javac 編譯的時候,並不像 C 和 C++ 那樣有“連接”這一步驟,而是在虛擬機加載 Class 文件的時候進行動態連接。

  也就是說,在 Class 文件中不會保存各個方法和字段的最終內存布局信息,因此這些字段和方法的符號引用不經過轉換的話是無法被虛擬機使用的。當虛擬機運行時,需要從常量池獲得對應的符號引用,再在類創建時或運行時解析並翻譯到具體的內存地址之中。關於類的創建和動態連接的內容,下篇博客會詳細介紹。

  常量池中的每一項內容都是一個表,在JDK1.8中共有 14 種結構各不相同的表結構數據,每個表結構第一位是一個 u1 類型的標志位(tag,取值為1 到 18,缺少標志為 2、13、14、17 的數據類型)。代表當前這個常量屬於哪種常量類型。

  14 種常量類型所代表的具體含義如下:

  

   接着看十六進制文件,緊跟常量池數量的十六進制是0x0a,這是一個標志位,0x0a的十進制數是10,查看常量池的項目表接口,表示的類型是 CONSTANT_Methodref_info。

   

 

   也就是說,接下來的u2類型0x0006,其十進制值為6,緊跟后面的u2類型十六進制為0x0017,其十進制值為23,這都是兩個索引值,分別指向第索引值為6的常量和索引值為23的常量。

  整個十六進制字節碼就不一一進行推導了,下面是各個數據類型的結構:

  

 

   

7、訪問標志

   常量池結束后的兩個字節表示訪問標志(access_flags),這個標識用於識別一些類或接口層次的訪問信息。

  包括:這個 Class 是類還是接口;是否定義為 public 類型,是否定義為 abstract 類型;如果是類的話,是否被聲明為 final 等。

  具體的標志位及標志含義如下:

  

 

   上表定義了 8 個標志位,但是我們說訪問標志是一個 u2 類型,一共有 32 個標志位可以使用,沒有定義的標志位一律為 0 。

8、類索引、父類索引和接口索引集合

  類索引、父類索引和接口索引按順序排列在訪問標志之后。

  類索引:用於確定這個類的全限類名 ,是一個 u2 類型的數據。

  父類索引:用於確定這個類的父類全限類名,也是一個 u2 類型的數據。因為Java是單繼承的,除了 java.lang.Object 類以外,所有的類都有父類。所以,除了Object 類以外,所有Java類的父類索引都不為0.

  接口索引:用於描述這個類實現了哪些接口,是一組 u2 類型的數據集合,第一項為 u2 類型的接口計數器,表示實現接口的個數。如果沒有實現任何接口,則為0。

9、字段表集合

   字段表(field_info):描述接口或類中聲明的變量。(不包括方法內部聲明的變量)

  描述的信息包括:

  ①、字段的作用域(public,protected,private修飾)

  ②、是類級變量還是實例級變量(static修飾)

  ③、是否可變(final修飾)

  ④、並發可見性(volatile修飾,是否強制從主從讀寫)

  ⑤、是否可序列化(transient修飾)

  ⑥、字段數據類型(8種基本數據類型,對象,數組等引用類型)

  ⑦、字段名稱

  前面5個修飾符,都是布爾值,用標志位來表示;后面兩個字段名稱和類型,是無法固定的,只能引用常量池中的常量來表示。

  

 

   access_flags 是一個 u2 類型,表示各種修飾符。

  

10、方法表集合

  Class 文件存儲格式中對方法的描述和字段的描述基本上是一致的。也是依次包括:

  訪問標志(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合數量(attributes_count)、屬性表集合(attributes)

  

 

 

  方法訪問標志如下(access_flags):

  

 

11、屬性表集合

  在前面介紹的字段表集合、方法表集合中都包括了屬性表集合(attributes),其實就是引用的這里。

  根據《Java虛擬機規范第二版》中,預定義了 9 項虛擬機實現應當能夠識別的屬性。

  

 

   對於每一個屬性,它的名稱要從常量池中引用一個 CONSTANT_Utf8_info 類型的常量來表示,其屬性值的結構則是完全自定義的,只需要說明屬性值所占用的位數長度即可。

   

參考文檔:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM