咱們平時在DOS界面中每每須要運行先運行javac命令,這個命令的直接結果就是產生相應的class文件,而後基於這個class文件才能夠真正運行程序獲得結果。天然。這是Java虛擬機的功勞,那麼是否是Java虛擬機只能編譯.java的源文件呢?答案是否認的。時至今日,Java虛擬機已經實現了語言無關性的特色。而實現語言無關性的基礎是虛擬機和字節碼的存儲格式,Java虛擬機已經不和包括Java語言在內的任何語言綁定。它只與「class」文件這種特定的二進制文件相關聯。在class文件中包含了Java虛擬機指令集和符號表以及若干輔助信息。能夠很容易想到Java(本質上不是Java語言自己的平臺無關性,而是其底層的Java虛擬機的平臺無關性使然。)的跨平臺,由於任何一門功能性語言均可以表示爲能被Java虛擬機接受的有效的class文件。好比,除了Java虛擬機能夠將Java源文件直接編譯爲class文件外,使用JRuby等其餘語言的編譯器同樣能夠把程序代碼編譯成class文件,因而可知,Java虛擬機並不關心class文件是由何種語言編譯來的。java
Class文件是一組以8字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊排列在class文件中,中間沒有任何分隔符,這使得class文件中存儲的內容幾乎是所有程序運行的程序。Java虛擬機規範規定,Class文件格式採用相似C語言結構體的僞結構來存儲數據,這種結構只有兩種數據類型:無符號數和表。數組
無符號數屬於基本數據類型,主要能夠用來描述數字、索引符號、數量值或者按照UTF-8編碼構成的字符串值,大小使用u一、u二、u四、u8分別表示1字節、2字節、4字節和8字節。安全
表是由多個無符號數或者其餘表做爲數據項構成的複合數據類型,全部的表都習慣以「_info」結尾。那麼表是幹嗎的呢?表主要用於描述有層次關係的複合結構的數據,好比方法、字段。須要注意的是class文件是沒有分隔符的,因此每一個的二進制數據類型都是嚴格定義的。具體的順序定義以下:數據結構
在class文件中,主要分爲魔數、Class文件的版本號、常量池、訪問標誌、類索引(還包括父類索引和接口索引集合)、字段表集合、方法表集合、屬性表集合。併發
頭4個字節是魔數,魔數的惟一做用在於肯定這個Class文件是不是Java虛擬機接受的Class文件。如gif和jpeg等在文件頭中都存在魔術,使用魔術而不是使用擴展名是基於安全性考慮的——擴展名能夠隨意被改變。Class文件的魔術值爲「0xCAFEBABE」(咖啡寶貝?)。佈局
緊接着魔數的4個字節是Class文件版本號:版本號又分爲次版本號和主版本號。其中前兩個字節用於表示次版本號,後兩個字節用於表示主版本號。這個的版本號是隨着jdk版本的不一樣而表示不一樣的版本範圍的。若是Class文件的版本號超過虛擬機版本,將被拒絕執行。性能
常量池能夠簡單理解爲class文件的資源從庫,這種數據類型是Class文件結構中與其餘項目關聯最多的數據類型,也是佔用Class文件空間最大的項目之一。在常量池中主要存放字面量和符號引用。字面量比較接近Java語言層面的常量概念,好比文本字符串、聲明爲final的常量值等(百度百科的解釋是字面量是用雙引用號引住的一系列字符)。符號引用則主要包括三類常量:編碼
符號引用與直接引用的關聯spa
符號引用是一組符號,用來描述所引用的目標,符號是以任何形式存在的字面量。對於符號引用Java虛擬機並無嚴格的限制。規定只須要使用的時候可以無歧義定位到目標就能夠。常量池存在於Class文件中,而Class文件是必須首先經過Java虛擬機的類加載機制加載到內存中(確切的說是方法區這個內存區域,回顧一下,方法區存放的主要是對象的實例,這個Class文件是虛擬機對外接受訪問的接口)。符號引用屬於常量池中的內容,那麼是否是說符號引用的目標已經加載到內存中了呢?答案是否認的,由於符號引用與虛擬機的內存佈局無關,符號引用的目標並不必定已經加載到內存中了。翻譯
直接引用能夠是直接指向引用目標的指針、相對偏移量或者是一個可以間接定位到目標的句柄。直接引用是和虛擬機的內存佈局有關的,同一個符號引用在不一樣的虛擬機上翻譯的直接引用通常是不一樣的。若是有了直接引用,那麼引用的目標一定是存在內存中的。
在常量池中每一項常量都是一個表,在jdk1.7中共有14中常量類型,因此常量池的項目就對應14張表,這14張表的每種類型都不同。可是有一個共同特色:表開始的第一位都是一個u1類型的標誌位,表明這個常量屬於哪一種類型。
須要注意的是,在Class文件中,方法、字段都須要引用CONSTANT-Utf8_info類型的常量,因此這種類型的常量的長度有必定的限制,也就是Java中方法、字段的最大長度。在CONSTANT-Utf8_info中,其length的值u2,說明Java虛擬機只能編譯最大大約64KB的變量或者方法名。超過的話將不會進行編譯。
常量池以後的數據結構是訪問標誌(access_flags),這個標誌主要用於識別一些類或者接口層次的訪問信息,主要包括:這個Class是類仍是接口、是否認義public、是否認義abstract類型;若是是類的話是否被聲明爲final等。具體的標誌訪問以下:
這個數據項主要用於肯定這個類的繼承關係。
其中類索引和父類索引都是一個u2類型的數據,而接口索引集合是一組u2類型的數據。在Java中因爲不容許多繼承,因此父類索引是惟一的,可是一個類能夠實現多個接口,因此獲得的接口索引是一個集合,表示這個類實現了哪些接口。
字段表用於描述接口或者類中聲明的變量。
字段包括類級變量和實例級變量,可是不包括方法內部聲明的局部變量(這些變量是存儲在Java虛擬機棧中的局部變量表中的)。天然,描述一個字段的信息包括:字段的做用域(public、protected、private)、實例變量與否(static)、可變性(final)、併發可見性(volatile)、能否被序列化(transient)、字段數據類型(基本數據類型、對象、數組)、字段名稱。字段的信息也被存放在一張表中,其字段表包括三種類型:
上面出現了簡單名稱,上文中出現了全限定名,以及這裏出現的描述符,三者有什麼區別呢?其中全限定名稱比較好理解,就是類的完整路徑信息。而簡單名稱則是指沒有類型和參數修飾的方法或者字段名稱,好比一個方法以下:
public void inc(int a,int b){ System.out.println(a+b); }
那麼這個方法的簡單名稱就是inc。
相對於以上二者,描述符相對複雜一些。描述符的主要的做用是描述字段的數據類型、方法的參數列表和返回值。其中咱們熟悉的void,在Class文件中用V表示。下面是完整的描述符標誌的含義:
對於數組類型,每一維度使用一個前置的「[」字符描述,若是是二維數組,那麼就有兩個「[」符號。好比「java.lang.String[][]」會被記錄成「[[Ljava.lang.String;」
對於方法,則是按照縣參數列表後返回值的順序進行描述的。好比方法int inc(int a,int[] b,char[][] c,int d)的描述符是「(I[I[[CI)I」。
JVM中堆方法表的描述與字段表是一致的,包括了:訪問標誌、名稱索引、描述符索引、屬性表集合。方法表的結構與字段表是一致的,區別在於訪問標誌的不一樣。在方法中不能用volatile和transient關鍵字修飾,因此這兩個標誌不能用在方法表中。在方法中添加了字段不能使用的訪問標誌,好比方法可使用synchronized、native、strictfp、abstract關鍵字修飾,因此在方法表中就增長了相應的訪問標誌。
要注意的是,若是父類方法沒有在子類中重寫,那麼在方法中不會自動出現來自父類的方法信息。一樣的,有可能添加編譯器自動增長的方法,好比方法。
前面的Class文件、字段表和方法表均可以攜帶本身的屬性信息,這個信息用屬性表進行描述,用於描述某些場景專有的信息。在屬性表中沒有相似Class文件的數據項目類型和順序的嚴格要求,只要新的屬性不與現有的屬性名重複,任何人均可以向屬性表中寫入本身定義的屬性信息。
Code屬性
Java程序方法體中的代碼通過javac編譯最終編譯成的字節碼指令就保存在Code屬性中。可是並不是全部的方法表都必須存在這個屬性。Code屬性是Class文件中最重要的一個屬性,若是把一個Java程序中的信息分爲代碼(Code)和元數據(Metadata,包括類、字段、方法定義及其其餘信息)兩部分,那麼在整個Class文件中,Code屬性用於描述代碼,全部其餘的數據項目都用於描述元數據。
Exceptions屬性
這個屬性的做用是列舉出方法中可能拋出的受查異常(Checked Exception),也就是描述throws 後的列舉的異常
LineNumberTable屬性
主要用於描述Java源代碼行號與字節碼行號之間的對應關係。這個屬性也不是必須的。若是沒有這個屬性,對程序的直接影響就是當拋出異常的時候沒法顯示對應的行號;而且在調試的時候沒法經過設置斷點的方法是調試程序。
LocalVariableTable屬性
用於描述棧幀中局部變量表中的變量與Java源碼中定義的變量的之間的關係。也不屬於必須的屬性。若是沒有這個屬性,產生的直接影響就是當別人引用這個方法的時候,全部的參數名稱都會丟失,IDE將會使用諸如args0、args1之類的參數進行顯示。天然,當調試程序的時候,顯示的參數名稱是不可知的。
SourceFile屬性
用於記錄這個Class文件的源碼文件名稱。若是不使用這個屬性,那麼當拋出異常的時候,堆棧中將不會顯示出錯代碼所屬的文件名。
ConstantValue屬性
做用是通知虛擬機自動爲靜態變量賦值。要注意的是,只有被static關鍵字修飾的額變量纔可使用這個屬性(類變量)。對於非類變量,初始化是在方法中進行的;對於類變量能夠選擇兩種方式進行變量的初始化:一是在類構造器方法中使用;二是是ConstantValue屬性。目前Sun Hotspot的選擇原則是:若是一個變量同時使用static和final關鍵字修飾,而且這個變量是基本數據類型或者java.lang.String類型的話,就使用ConstantValue屬性進行初始化。若是沒有被final修飾或者並不是是基本數據類型,那麼將會選擇使用方法進行初始化。
InnerClass屬性
這個屬性主要用於記錄內部類與宿主類之間的關聯關係。
Deprecated以及Synthetic屬性
這兩個屬性都屬於標誌類型的布爾屬性,只存在有沒有的區別。
Deprecated屬性用於表示某個類、字段或者方法,已經被程序做者定爲再也不推薦使用,能夠經過註解@deprecated實現
Synthetic屬性表明此字段並非由Java源碼產生的,而是經過編譯器自行添加的。
StackMapTable屬性
該屬性的目的在於代替之前比較消耗性能的基於數據流分析的類型推導驗證器。
Signature屬性
這個屬性是專門用來記錄泛型類型的,由於在Java語言採用的是擦除法實現的泛型,在字節碼(Code屬性)中,泛型信息編譯以後會被擦除。擦除法的優勢是可以節省泛型所佔的內存空間,缺點是在運行期間沒法經過反射獲得泛型信息,而Signature屬性則彌補了這一缺陷。如今的Java反射API已經可以獲得泛型信息,功勞就在於這個屬性。
BootstrapMethods屬性
這個屬性用於保存invokedynamic指令引用的引導方法限定符。該指令用於在運行時動態解析出調用點限定符所引用的方法,並執行該方法。
參考 一、周志明,深刻理解Java虛擬機:JVM高級特性與最佳實踐,機械工業出版社