代碼編譯的結果從本地機器碼轉爲字節碼,是儲存格式發展的一小步,倒是編程語言的一大步。——《深刻理解Java虛擬機》
計算機只認識0和1.因此咱們寫的編程語言只有轉義成二進制本地機器碼才能讓機器認識。然而隨着虛擬機的發展,包括Java在內的不少語言,都選擇了一種和操做系統、機器指令集無關的中立儲存格式來儲存編譯後的數據。編程
咱們都知道Java經典標語,「一次編譯,處處運行」。實現這一目標,每一個平臺上定製的虛擬機,須要讀取統一的數據。這種數據不依賴於任何一種平臺,甚至不關心是由哪一種語言編譯來的,只要統一了格式,虛擬機就能正確的使用它。這種統一的格式就是——字節碼(Class文件)。數組
Class文件中儲存了Java虛擬機指令集和符號表以及若干其餘輔助和結構化約束。處於安全考慮,Class文件中使用了許多強制性的語法和結構化約束。安全
下面來看下本文的硬菜,Class文件的結構。雖然說大佬書中是以JDK1.4爲版本講述的,可是它所包含的指令、屬性是Class文件中最重要最基礎的。後續不一樣的版本都是對它的加強。架構
任何一個Class文件都對應着惟一一個類或者接口的定義信息,可是反過來講,類和接口並不必定都得定義在文件裏(譬如類和接口也能夠經過類加載器直接生成)。編程語言
Class文件是以一組以8位字節爲基礎單位的二進制流,這個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中儲存的內容幾乎是程序運行的必要數據,沒有空格存在。編輯器
Class有兩種數據類型(雖然用十六進制編輯器打開,看上去都是十六進制字符):無符號數和表。無符號數能夠用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。表是由多個無符號數或者其餘表做爲數據項構形成的符合數據類型,全部表都習慣性地以「info_」結尾。表用於描述層次關係的複合結構數據,整個Class文件實質上就是一張表。this
其中相似於緊挨着的constant_pool_count、constant_pool 這樣的數據可視爲一個總體(一個表),前面記錄後者數據的數量。編碼
看class文件結構那張表,第一個就是u4 magic。這是一個佔了4個字節的魔數,它的惟一做用就是肯定這個文件是否爲一個Class文件。它就是一個標誌,告訴虛擬機本身是Class文件,這樣作更加安全,四個字節儲存的值是固定的,十六進制下爲「0xCAFEBABE」,咖啡寶貝。spa
接下來分別是兩個字節的minor(次版本)和兩個字節的major(主版本)。分別儲存着此Class文件時何種版本的編譯器編譯的,例如50.3,50就是主版本3就是次版本。在運行時能夠向下兼容,好比51版本虛擬機能夠運行50.3版本的class文件,可是反過來就不行了。操作系統
緊接着 constant_pool_count、constant_pool就是常量池部分。常量池能夠理解爲Class文件的資源倉庫,它是Class文件結構中與其餘項目關聯最多的數據類型,也是佔用Class文件最大的數據項目之一。
首先兩字節的constant_pool_count是統計後面constant_pool的常量數量的。注意後面的數量是從1開始,例如constant_pool_count儲存的數字是22,那麼constant_pool中就儲存了21個數據項。這麼設計是爲了讓「第0個位置」儲存寫特殊的數據。Class文件只有這一部分計數是從1開始的,其餘部分仍是從0開始。
常量池中主要儲存兩大類常量:字面量和符號引用。字面量好理解就是注入字符串、final修飾的常量值等等。符號引用主要包含一下三個常量:
Class文件中不會儲存各個方法、字段的最終內存分佈,只有在執行到特定的代碼時纔會知道真正的內存入口(某信息的地址)。在JDK1.4中,常量池可包含的常量項以下(之後的版本會對內容進行擴充):
最麻煩的這些類型分別有本身的結構,不過共同的特色是第一個字節都儲存着tag,即告訴虛擬機本身那種常量項。從這部份內容能夠看出不少東西,好比說一個變量名稱最大時兩個字節,即64KB英文字符大小,固然按常理來講不會出現這樣變態的變量名吧。
在常量池結束以後,緊接着兩個字節表明訪問標誌(access_flags),這個標誌用於識別一些類或者接口層次的訪問信息。
訪問標誌使用或來計算,好比一個類被ACC_PIBLIC(0x0001)、ACC_SUPER(0x0020)所修飾,那麼計算爲0x0001|0x0020 = 0x0021,該值就是被訪問標誌儲存的值。Java中有專門計算關鍵字的包。
類索引(this_class)和父類索引(super_class)都是一個u2類型的數據,而接口索引是一組u2類型的數據的集合。Class文件中有這三項來肯定繼承關係。除了Object類之外,全部的夫索引都不是0。若是結構計數器的大小是0,那麼後面那部分就沒有數據。
字段表用於描述接口或者類中聲明的變量。字段包括類級變量和實例級(對象級)變量,但不包括方法內部的局部變量。如下是字段表結構和字段表的第一個屬性訪問標誌。
access_flags 的計算方式和前面類或者接口的訪問表示相同。後面緊跟着兩個屬性是name_index
和 descriptor_index,分別表明着簡易名稱和方法描述符。
字段表集合中不會列出從超類或者父接口中繼承下來的字段,可是能夠列出原本Java代碼中不存在的字段,譬如在內部類中爲了保持對外部類的訪問性自動添加的字段。另外在Java中,同一個類不能出現簡易名稱相同的字段名,例如int name,後面緊跟着String name。可是在字節碼層面,簡易名稱能夠相同,後面的描述不一樣就行了。
方法表的結構和字段表的機構基本相似。
與字段表集合相對應,若是父類方法在子類中沒有被重寫,方法表集合中就不會出現父類的方法信息。在Java語言中,要重載一個方法,除了要與原方法具備相同的簡單名稱以外,還要求擁有一個與原方法不一樣的特徵簽名。特徵簽名就是一個方法中各個參數在常量池中的字段符號引用的集合,也就是由於返回值不會包含在特徵簽名中,因此僅僅是返回值不一樣,不是重載。
在Class文件、字段表、方法表都攜帶本身的屬性表集合。屬性表的數據項目相對於其餘部分比較寬鬆一點,可是內容也有不少。下面來看一下比較重要的。
Java類的程序方法體中的代碼通過編譯後儲存在Code屬性中,可是接口和抽象類中的方法就不存在Code屬性中。
max_locals表明了局部變量表所須要的儲存空間,其中最小單位是Slot。其中Slot能夠複用,當代碼執行超出一個局部變量的做用域時,這個局部變量所佔的Slot能夠被其餘局部變量所使用,極大節省了空間。
code_length和code值儲存的時Java源代碼編譯後生成的字節碼指令。因爲每一個code只佔了一個字節,因此能表示的指令數只有256個。code_length的長度雖然時四個字節,可是因爲虛擬機的規定只能使用兩個字節,因此最大隻能編譯65535條指令,通常來講也是夠用了,可是在編譯複雜的JSP的時候要注意,某些編譯器會把JSP內容和頁面輸出的信息歸併於一個方法中,就可能致使編譯失敗。
值得一提的是,Javac在編譯方法的時候,參數即便你沒有填,agrs_size也多是1,這是因爲隱式傳進去了this,固然static修飾的方法參數就是0(不填寫的狀況下)。
曾經使用try-catch的時候,注意到finlly不會改變局部變量的值,覺得是try已經return了,return以後纔去執行的finlly中的數據,其實否則。例以下面這段代碼。
public int inc(){ int x; try{ x=1; return x; }catch(Exception e){ x=2; return x; }finally{ x=3; } }
這段代碼永遠不會輸出x=3,執行順序是這樣的(以不會拋出異常爲例):首先執行x=1,此時局部變量等於1.而後讀到return指令,而後將x的值賦給一個空間,這個空間是return時返回的值,咱們暫且將這塊空間起個名字,叫作returnX,而後代碼進入finally,注意此時,還在這個inc()方法的做用域中。而後將x賦值等於3,最後執行return指令,返回剛纔那塊returnX空間的值給調用者。離開inc()做用域,此時x那塊Slot能夠被複用。
字節碼指令不會超過256個,通常來講一個指令後面會跟着參數,這很天然,就像咱們寫方法時須要加入參數(沒有參數也是種參數)。可是因爲Java虛擬機採用面向操做數棧而不是寄存器(編譯語言)的架構,因此大多數狀況下只包含一個操做碼。
因爲字節碼數量有限,因此不少指令會被強制統一。好比處理boolean、byte、short和char類型的數組時,也會轉化爲對應的int類型的字節碼指令來處理。
字節碼操做的時候可能會致使溢出,例如兩個很大的正整數相加,結果可能會稱爲一個負數。當一個操做產生溢出時,將會使用有符號的無窮大來表示,若是某個操做結果沒有明確的數字定義的話,將會使用NaN值來表示。全部使用NaN值做爲操做數的算術操做,結果會返回NaN。
Java虛擬機能夠支持方法級的同步和方法內部一段指令序列的同步,這兩種同步結構都時使用管程(Monitor)來支持的。能夠看做Synchronized此時拿的鎖就是Monitor。