類文件結構——深入理解Java虛擬機 筆記三
在之前的筆記中記錄過,Java程式變成可執行文件的步驟是:源程式碼——>經過編譯變成class文件——>經過JVM虛擬機變成可執行的二進位文件。因此,為了對JVM執行程式的過程有一個好的了解,我們需要先明白class文件到底是什麼東西,它裡面有那些資訊以及如何存儲的。
Class類文件結構
Class文件是一組以8位位元組為基礎單位的二進位流,各個數據項目嚴格按照順序緊湊排列在Class文件中。當遇到需要佔用超過8位位元組的數據項時,則會按照高位在前的方式分割成若干個8位位元組進行存儲。
Class文件格式採用一種類似於C語言結構體的偽結構來存儲,這種偽結構中只有兩種數據類型:無符號數和表。
無符號數是基本的數據類型,以u1,u2,u4,u8分別表示1個位元組、2個位元組、4個位元組、8個位元組的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成的字元串值。
表是由多個無符號數或者其他表作為數據項構成的複合數據結構,所有表習慣性地以_info結尾。
我們先來看一下Class文件的格式,然後對這些數據一一進行講解。

魔數和Class文件的版本
每個Class文件的頭四個位元組稱為魔數,作用是來表示該文件是一個可以被虛擬機接受的Class文件。其作用類似於擴展名,但擴展名易更改,而魔數來進行類型判別比較安全。
緊接著的四個位元組存儲的是Class文件的版本號。第5、6位元組為次版本號,第7、8位元組為主版本號。

常量池
版本號之後的就是常量池。常量池是Class文件結構中與其他項目關聯最多的數據類型,也是佔用Class文件空間最大的數據項目之一。
常量池中主要存放兩大類常量,字面量和符號引用。字面量比較接近於Java語言的常量概念,比如文本字元串、被聲明為final的常量值等等而符號引用屬於編譯原理方面的概念,包括了下面三類常量:1、類和介面的全限定名 2、欄位的名稱和描述符 3、方法的名稱和描述符
Java程式碼進行編譯的時候,沒有C那樣用"鏈接"這一步驟,而是在虛擬機載入Class文件的時候進行動態鏈接。也就是Class文件不會保存各個方法和欄位的最終記憶體布局資訊,因此這些欄位和方法的符號引用不經過轉換的話是無法被虛擬機使用的。當虛擬機運行的時候,需要從常量池獲得對應的符號引用,再在類創建或運行的時候解析並翻譯到具體記憶體地址中 ??
對這些一一列舉也沒有什麼意義,如果需要某一項的內容,可以單獨查找。本章我認為更關鍵的是對Class文件怎麼生成的,怎麼運作的理解。
在常量池中,記載了各種常量(包括系統執行時內部需要的)、方法表(用來記錄該類的所有方法)、屬性表等。他們的運作方法大同小異:通過已經定義的常量(包括字元串常量、方法名之類的資訊)來填充對應的表資訊,比如方法表中,我們需要得到方法名、返回值、參數等等資訊,這些資訊都存儲在常量池中,只需要引用到對應位置即可表示。而方法表中更為關鍵的資訊就是Code屬性了,Code屬性表示了該方法的執行過程。
先看一下Code屬性表的內容:

裡面像attribute_name_index之類都是索引,指向常量池中的一個字元串來表示對應的數據常量。關鍵的地方是code_length和後面的code,他們用來存儲Java源程式碼編譯後生成的位元組碼指令。code_length程式碼位元組碼長度,code用於存儲位元組碼指令的一系列位元組流,每一個指令都是一個u1類型的單位元組。,當虛擬機讀到對應位元組碼的位元組後,他會查詢出是什麼指令並執行。

通過指令的分析,我們可以看到,通過位元組碼我們可以實現利用常量池中的數據資訊,來分析運行結果並執行的。
在這個位元組碼中,它執行的數據交換、方法調用都是基於操作棧進行的,比如2A將對象移動到棧頂,B7是以棧頂對象為接受者,他們都是對一個棧進行的操作,但又有像invokespecial這樣帶參數的指令,這就與單純使用堆棧進行操作不相符,因此另一個問題是JVM如何堆位元組碼執行。
直接使用javap -verbose 類的class文件 即可得到該class的內容。
