咱們知道計算機是由晶體管、電路板等組裝而成的電子設備,而這些電子設備其實只能識別0與1的信號。html
那麼問題來了,咱們在操做系統上編寫的Java代碼(由字母、數字等各類符號組成),打包後部署到服務器上,是如何被計算機所識別並運行的呢?另外,操做系統有不少種,包括Windows系統,Linux系統,Mac OS系統等,而咱們一樣的Java代碼,卻能夠不作任何處理在不一樣的系統上正常運行,這又是爲啥呢?java
帶着這些疑問,你將會在下面的介紹中獲得答案!!!數組
在此係列博客第一篇文章中,咱們介紹到Java虛擬機的兩個特性。安全
對於Java語言,咱們經過編輯器編寫的Java代碼,後綴通常是.java。經過javac編譯器編譯後,會變成.class結尾的字節碼文件,只有編譯後的.class文件,才能在Java虛擬機上運行。(解壓部署在服務器上的jar包,全是編譯後的class文件)ruby
再好比對於 JRuby 語言,經過編輯器編寫的代碼後綴是.rb。經過jrubyc 編譯器編譯後,也會變成 .class 結尾的字節碼文件,而後也能在Java虛擬機上運行。服務器
在好比已正式成爲Android官方支持開發語言的Kotlin。也能夠編譯成.class字節碼文件,而後在虛擬機上運行。併發
咱們能夠用下面這幅圖來表示:oracle
也就是說,無論你是什麼語言,只要能經過某種手段生成合乎規範的.class字節碼文件,其實就能夠在Java虛擬機上運行,這就是語言無關性。jvm
Write once, run everywhere(一次編寫,處處運行)這是Java語言誕生之處就宣傳的一個口號。Java語言之因此可以跨平臺運行,其實就是由於Java虛擬機對各個平臺的適配,在不一樣的系統下安裝不一樣的Java虛擬機,咱們程序固然可以在不一樣的系統上運行。編輯器
對於文章開頭提出的問題,一樣的程序可以在不一樣的系統上正常運行的緣由,就是由於咱們在不一樣的系統上安裝了不一樣的Java虛擬機。
搞清楚了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)
打開後的文件以下:(下面的介紹也都是以這張圖爲例)
下面咱們會介紹這些十六進制分別表明什麼意思。
另外,爲了更好的查看 Class 文件字節碼結構,JDK 還爲咱們提供了一個命令行工具 javap。使用語法以下:
javap <options> <classes>
經過 javap -help 命令,能夠查看相關參數做用:
咱們將 ClassTest.class 文件,經過 javap -v ClassTest.class 命令,執行後以下:
這些內容下面也會詳細介紹。
在介紹這些十六進制以前,咱們先介紹 Class 文件的數據類型。
Class 文件採用一種相似於 C 語言結構體的僞結構來存儲,這種僞結構只有兩種數據類型:無符號數和表。
①、無符號數
這是一種基本數據類型,以 u1,u2,u4,u8 來分別表明 1個字節、2個字節、4個字節、8個字節的無符號數,無符號數能夠用來描述數字、索引引用、數量值或按照 UTF-8 編碼構成的字符串值。
②、表
表是由多個無符號數或其它表做爲數據項所構成的複合數據類型,全部表都習慣行的以「_info」結尾。表用於描述有層次關係的複合結構數據。
整個 Class 文件本質上就是一張表,結構以下:
PS:須要說明的是,因爲 Class 文件結構沒有任何分隔符,因此不管是每一個數據項的的順序仍是數量,都是嚴格限定的,哪一個字節表明什麼含義,長度多少,前後順序如何,都是不容許改變的。
下面,咱們就來分別介紹這些數據項表明什麼含義。
每一個 class 文件的頭 4 個字節稱爲魔數(Magic Number),它的惟一做用是:標識該文件是一個Java類文件。若是沒有識別到該標誌,則說明該文件不是Java類文件或者文件已受損。
由上圖,咱們能夠看到前 4 個字節是 cafe babe。這是 Gosling 定義的一個魔法數,意思是 Coffee Baby。
其實不少文件存儲標準中都使用魔數進行身份識別,好比圖片gif或者jpeg,使用魔數而不是使用擴展名來進行識別主要是基於安全考慮,由於文件擴展名能夠任意的改動。
緊隨魔數的 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 文件,即便文件格式未發生變化。
上圖第五、六、七、8個字節爲 00 00 00 34。其十進制值爲 52,是JDK8的內部版本號。
緊隨主版本號的是常量池入口,是class文件中第一個出現的表類型數據項目,也是佔用Class文件空間最大的項目之一,更是Class文件結構中與其它項目關聯最多的數據類型。
由於常量池中常量的數量是不固定的,因此在常量池的入口要放置一項 u2 類型的數據,表明常量池容量計數值(constant_pool_count)。
PS:注意,常量池容量計數值是從 1 開始的,而不是從 0 開始。將 0 空出來,是爲了知足後面某些指向常量池的索引值的數據在特定狀況下須要表達「不引用任何一個常量池項目」的意思。
Class 文件結構中,只有常量池的容量是從 1 開始的,其它的集合類型,都是從 0 開始的。
看上圖的十六進制文件,常量池容量計數值爲:0x0025,即十進制 37。這就表示常量池中有 36 項常量,索引值分別爲 1~36(經過上面javap命令生成字節碼文件能夠很明顯看出來有36個)
常量池主要存放兩大類常量:
一、字面量(Literal):字面量比較接近於 Java 語言層面的常量概念,好比 文本字符串、被聲明爲 final 的常量值等。
二、符號引用(Symbolic References):符號引用屬於編譯原理方面的概念,包括下面三類常量:
類和接口的權限定名(Fully Qualified Name)
字段的名稱和描述符(Descriptor)
方法的名稱和描述符。
須要說明的是,Java代碼在進行javac 編譯的時候,並不像 C 和 C++ 那樣有「鏈接」這一步驟,而是在虛擬機加載 Class 文件的時候進行動態鏈接。
也就是說,在 Class 文件中不會保存各個方法和字段的最終內存佈局信息,所以這些字段和方法的符號引用不通過轉換的話是沒法被虛擬機使用的。當虛擬機運行時,須要從常量池得到對應的符號引用,再在類建立時或運行時解析並翻譯到具體的內存地址之中。關於類的建立和動態鏈接的內容,下篇博客會詳細介紹。
常量池中的每一項內容都是一個表,在JDK1.8中共有 14 種結構各不相同的表結構數據,每一個表結構第一位是一個 u1 類型的標誌位(tag,取值爲1 到 18,缺乏標誌爲 二、1三、1四、17 的數據類型)。表明當前這個常量屬於哪一種常量類型。
14 種常量類型所表明的具體含義以下:
接着看十六進制文件,緊跟常量池數量的十六進制是0x0a,這是一個標誌位,0x0a的十進制數是10,查看常量池的項目表接口,表示的類型是 CONSTANT_Methodref_info。
也就是說,接下來的u2類型0x0006,其十進制值爲6,緊跟後面的u2類型十六進制爲0x0017,其十進制值爲23,這都是兩個索引值,分別指向第索引值爲6的常量和索引值爲23的常量。
整個十六進制字節碼就不一一進行推導了,下面是各個數據類型的結構:
常量池結束後的兩個字節表示訪問標誌(access_flags),這個標識用於識別一些類或接口層次的訪問信息。
包括:這個 Class 是類仍是接口;是否認義爲 public 類型,是否認義爲 abstract 類型;若是是類的話,是否被聲明爲 final 等。
具體的標誌位及標誌含義以下:
上表定義了 8 個標誌位,可是咱們說訪問標誌是一個 u2 類型,一共有 32 個標誌位可使用,沒有定義的標誌位一概爲 0 。
類索引、父類索引和接口索引按順序排列在訪問標誌以後。
類索引:用於肯定這個類的全限類名 ,是一個 u2 類型的數據。
父類索引:用於肯定這個類的父類全限類名,也是一個 u2 類型的數據。由於Java是單繼承的,除了 java.lang.Object 類之外,全部的類都有父類。因此,除了Object 類之外,全部Java類的父類索引都不爲0.
接口索引:用於描述這個類實現了哪些接口,是一組 u2 類型的數據集合,第一項爲 u2 類型的接口計數器,表示實現接口的個數。若是沒有實現任何接口,則爲0。
字段表(field_info):描述接口或類中聲明的變量。(不包括方法內部聲明的變量)
描述的信息包括:
①、字段的做用域(public,protected,private修飾)
②、是類級變量仍是實例級變量(static修飾)
③、是否可變(final修飾)
④、併發可見性(volatile修飾,是否強制從主從讀寫)
⑤、是否可序列化(transient修飾)
⑥、字段數據類型(8種基本數據類型,對象,數組等引用類型)
⑦、字段名稱
前面5個修飾符,都是布爾值,用標誌位來表示;後面兩個字段名稱和類型,是沒法固定的,只能引用常量池中的常量來表示。
access_flags 是一個 u2 類型,表示各類修飾符。
Class 文件存儲格式中對方法的描述和字段的描述基本上是一致的。也是依次包括:
訪問標誌(access_flags)、名稱索引(name_index)、描述符索引(descriptor_index)、屬性表集合數量(attributes_count)、屬性表集合(attributes)
方法訪問標誌以下(access_flags):
在前面介紹的字段表集合、方法表集合中都包括了屬性表集合(attributes),其實就是引用的這裏。
根據《Java虛擬機規範第二版》中,預約義了 9 項虛擬機實現應當可以識別的屬性。
對於每個屬性,它的名稱要從常量池中引用一個 CONSTANT_Utf8_info 類型的常量來表示,其屬性值的結構則是徹底自定義的,只須要說明屬性值所佔用的位數長度便可。
參考文檔:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.4