歡迎來到“Under The Hood”第二期。上期我們討論了抽象計算機JVM。如果你對JVM還很陌生,你可以去看看上期的文章。本期,我們稍稍窺探一下Java類文件的基本結構。
為旅行而生
Java類文件(.class文件)是一個為已編譯Java程序仔細定義的格式。Java源代碼被編譯成能夠被任何JVM加載和執行的類文件。在被JVM加載之前,類文件可能是由網絡傳輸而來。
類文件是獨立于底層平臺的,所以適用于更多的地方。它們由簡潔的JVM字節碼組成,這樣就能輕裝上陣。類文件常常被壓縮,以極快的速度通過網絡,到達世界各地的JVM。
類文件里有什么?
Java類文件包含JVM需要知道的關于一個Java類或接口的一切。按照它們的出現次序,主要的部分有:魔法數(magic),版本號(version),常量池(constant pool),訪問標示符區(access flags),當前類區(this class),超類區(super class),父接口區(interfaces),字段區(fields),方法列表區(methods),屬性區(attributes)。
保存在類文件中的信息經常在長度上有變化,所以信息的實際長度在被加載之前不能被預測。例如,在方法區里的方法數目,類與類之間是不相同的,這取決于源代碼中定義的方法個數。類文件中,這些信息的實際大小或長度,被安排在信息內容之前。這樣,當類文件被JVM加載時,可變信息的長度首先被讀取。一旦JVM知道信息的大小,它就能正確的讀取實際的信息內容。
類文件中,不同的相鄰信息之間通常沒有空白或填充字符;一切都以字節(byte)邊界對齊。這使得類文件很小,適合網絡傳輸。
為了讓JVM在加載類文件時,知道需要什么信息以及從哪里可以取得所需信息,類文件的各個組成部分的次序是嚴格定義的。例如,每個JVM都知道類文件的前8個字節由魔法數和版本號組成,常量池從第9個字節開始,訪問標示符區緊跟在常量池后面。但是,因為常量池的長度是可變的,在讀取完常量池之前,JVM是不知道訪問標示符區具體從什么地方開始。一旦讀取完常量池,JVM就知道接下來的2個字節就是訪問標示符區。
魔法數(Magic)和版本號(Version)
每個類文件的開始4個字節都是0xCAFEBABE。這個神奇的數字讓Java類文件更容易識別,因為類文件以外的文件幾乎不可能也以這四個相同的字節開頭。之所以稱之為魔法數,是因為它可以被文件格式設計者們從帽子里拉出來(??)。對它僅有的要求是,不能被現實已有的文件格式占用。根據最初Java團隊主要成員之一的Patrick Naughton所說,遠在“Java”被當作Java語言的名稱之前,這個神奇的數字就已經被選好了。我們當時在尋找一個有趣,獨特并且很容易記住的數字。0xCAFEBABE作為漂亮的Peet’s Coffee的咖啡師的代稱,能預示未來Java語言的名字,這完全是一個巧合。
類文件接下來的4個字節包含了大版本號(major version)和小版本號(minor version)。這些數字標識了特定類文件使用的類文件格式,讓JVM可以驗證類文件是否可以被載入。每個JVM都有一個它能載入的最大版本號,拒絕加載大于最大版本號的類文件。
常量池(Constant Pool)
類文件在常量池中保存與類或接口關聯的常量。常量池中能看到的部分常量是字符串字面值(literal strings),final變量的值(final variable values),類名,接口名,變量名和變量類型,方法名和方法簽名(method names and signatures)。方法簽名由方法返回值類型(return type)和一組參數類型(argument types)組成。
常量池被組織成一個元素長度可變的數組。每個常量占據數組中的一個元素。在整個類文件中,常量通過指示它們在數組中位置的整型索引來引用。第一個常量的索引值是1,第二個是2,以此類推。常量池數組的元素個數寫在常量池的前面,所以在加載類文件時,JVM知道它需要加載多少常量。
常量池中每個元素以指明自己類型的單字節標簽(tag)開始。一旦JVM看到這個標簽,就能知道接下來會遇到什么類型的常量。例如,如果看到一個表示字符串的標簽,JVM會認為接下來2個字節就是字符串的長度,然后就是“長度”個字節組成的字符串。
在本文剩下的部分,我有時會用constant_pool[n]表示常量池數組的第n個元素。從常量池組織的像個數組來說,這是有道理的;但是請記住,這些元素具有不同的大小和類型,并且第一個元素的索引是1。
訪問標識符區(Access Flags)
常量池之后的2個字節就是訪問標示符,它表明該文件定義的是類還是接口;該類或接口是公開的(public)還是抽象的(abstract);如果是類,該類是不是final的。
當前類區(This class)
接下來2個字節是當前類區,它是常量池數組的索引。被當前類引用的常量constant_pool[this_class],包含兩部分:單字節標簽(tag)和雙字節名稱索引(name index)。標簽等于CONSTANT_Class,一個表示本元素中包含類或接口信息的值。constant_pool[name_index]是一個包含類或接口名的字符串常量。
當前類部分稍稍揭示了常量池是怎么被使用的。當前類區本身只是一個常量池的索引。當JVM查找constant_pool[this_class]時,它找到一個用標簽表明自己是一個CONSTANT_Class得元素。JVM知道CONSTANT_Class元素在標簽(tag)之后,總是有一個叫名稱索引(name index)的常量池雙字節索引。然后它查找constant_pool[name_index],得到包含類或接口名的字符串。
超類區(Super class)
當前類區之后是超類區,也是2個字節的常量池索引。constant_pool[super_class]是CONSTANT_Class元素,它指向當前類所直接繼承的超類名。
接口區(Interfaces)
接口區開頭的2個字節,表示文件所定義的類(或接口)實現的接口數目。緊接著是一個數組,它包含了類所實現的每一個接口在常量池中的索引。
每個接口都是常量池中的CONSTANT_Class元素,它指向接口名。
字段區(Fields)
字段部分,以表示該類或接口包含的字段數的2個字節開始。字段是一個實例變量,或者是類或接口的類變量。接下來是一個以可變長結構為元素的數組,一個結構一個字段。每個結構都包含一個字段的相關信息,如字段名,字段類型,如果是final變量,還包括字段值。部分信息在結構當中,另一部分在常量池中由結構所指向的位置。
這部分僅有的字段,都是由定義在該類文件中的類或接口聲明的變量;繼承自超類或接口的字段不在此列。
方法區(Methods)
方法部分,以表示類或接口中方法數目的2字節開始。這個數目,只包含當前類顯式定義的方法,不包括繼承自超類的方法。數目之后是方法本身。
表示每個方法的結構包含方法相關的幾條信息,包括方法描述符(method descriptor,包括返回值類型和參數列表),方法本地變量需要的棧字(stack words)數,方法操作數棧(operand stack)需要的最大棧字數,方法捕獲的異常表,字節碼序列和行號表。
屬性區(Attributes)
排在最后的是屬性區,它提供定義在類文件中的特定類或接口的一般信息。屬性區以2字節的屬性數目開始,然后是屬性本身。比如一個表示源碼屬性的屬性:它表示當前類被編譯而來的源文件名。JVM會悄悄地忽略任何它們識別不了的屬性。
本文譯自:The Java class file lifestyle