Java 實現一次編譯到處運行的基礎,來源於 Java 虛擬機屏蔽了操作系統的底層細節。使用 class 文件存儲編譯後的源程序,使得 Java 程序的編譯與操作系統解耦。正是因為 Java class 文件的設計與 Java 語言解耦,分別發佈了 Java語言規範和 Java 虛擬機規範,使得其他語言如Scala、Groovy、JRuby、JPython 等基於Java 虛擬機的語言按照 class 文件格式要求生成的class 文件也能在虛擬機上運行。
class 文件格式
class 文件採用如下的結構存儲二進制內容。其中 u2、u4 分別表示佔用 2、4 個字節。
{
u4 magic; //魔數,固定為0xCAFEBABE
u2 minor_version; //次版本號
u2 major_version; //主版本號
u2 constant_pool_count; //常量池計數器
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //訪問標誌,聲明權限
u2 this_class; //類索引
u2 super_class; //父類索引
u2 interfaces_count; //接口個數
u2 interfaces[interfaces_count]; //接口列表
u2 fields_count; //字段個數
field_info fields[fields_count];//字段列表
u2 methods_count; //方法個數
method_info methods[methods_count]; //方法列表
u2 attributes_count; //屬性個數
attribute_info attributes[attributes_count]; //屬性列表
}
class 文件內容解讀
常量池:
存儲 class 文件用到的所有的字符串常量、類名、接口名、字段名以及其他常量。class 文件的其他項目往往會引用常量池中的常量,因此常量池容量計數從1開始,0 用於表示其他項目不引用常量池。在常量池中主要存儲了字面量和符號引用兩大類常量,字面量主要是字符串、final 類型常量值等,符號引用則包括類和接口的全限定名、字段的名稱和描述法以及方法的名稱和描述符。在前文《 Java 虛擬機類加載機制》中提到的符號引用轉換為直接引用中的符號引用就是常量池中的符號引用。
訪問標誌:
類或接口的訪問權限信息,包括 public、final、super、interface、abstract、annotation、enum 幾種屬性,以及使用 synthetic 表示非 Java 源碼生成的代碼。
類索引:
this_class 存儲常量池中的一個索引,索引處的常量表示 class 文件定義的類或接口。如果這是一個類,super_class 為 0 或存儲常量池中的一個索引,索引處的常量表示父類;如果這是一個接口,super_class 存儲常量池中的一個索引,索引處的常量一定是 java.lang.Object。通過 this_class 可以確定當前類的全限定名,通過 super_class 可以確定父類的全限定名。
接口列表:
如果這是一個類,存儲該類實現的接口列表,按照 implements 後的接口順序存儲;如果這是一個接口,存儲該接口的所有父接口列表,按照 extends 後的接口順序存儲。
字段列表:
存儲類或接口聲明的變量,包括類變量和實例變量。描述了每個變量的信息,包括作用域、static、final、volatile、transient、類型、名稱等。其中字段的名稱、類型需要引用常量池中的常量來描述。
方法列表:
存儲類或接口聲明的方法,包括類方法和實例方法。描述了每個方法的信息,包括訪問標誌、名稱索引、描述符索引、屬性表集合等。這裡僅僅存儲了方法的信息,方法的實現代碼編譯成字節碼後存儲在屬性表集合中的 “ Code ” 屬性裡面。
屬性列表:虛擬機規範定義了大量的屬性,class 文件、字段列表、方法列表都可以使用屬性描述專有信息。而屬性的名稱需要引用常量池的常量來表示。方法體中的代碼經編譯後就存放在名為 Code 的屬性中。
總結
Java 源程序編譯後生成 class 文件而不是二進制可執行文件,通過 Java 虛擬機來解析並執行 class 文件中的程序,實現了“一次編譯,到處運行”。在 class 文件中,存儲了類或接口的基本信息,如版本號、類名、接口列表、字段列表、方法列表等。