Java虛擬機只與Class文件相關聯,它規定了Class文件應該具備的格式,而不論該文件是由什麼語言編寫並編譯而來。因此,任何語言只要可以最終編譯成符合Java虛擬機要求的Class文件,就能夠運行在Java虛擬機上面。就是說,不管是使用Java, Scala, Kotlin, Groovy仍是其餘語言,只要編譯出的Class文件符合虛擬機規範,那麼均可以被虛擬機執行。因此,實際上Java規範就是由Java語言規範和Java虛擬機規範兩個獨立的部分組成。java
Class類文件是一種二進制文件,它包含了Java虛擬機指令集和符號表以及若干其餘輔助信息。Class文件格式採用相似於C的僞結構來存儲數據,這種結構只有兩種數據類型:無符號數和表。無符號數屬於基本數據類型,以u1
,u2
,u4
,u8
分別表明1字節、2字節、4字節和8字節的無符號數,無符號數能夠用來描述數字、索引、數量值或者按照utf-8編碼構成的字符串。bash
表是由多個無符號數或者其餘做爲表做爲數據項構成的複合數據結構,全部表習慣性地以"_info"結尾,整個Class文件本質上是一張表。而所謂的表就對應於C++中的一個結構體,好比整個Class文件對應的結構體就是:網絡
struct ClassFile {
u4 magic; // 識別Class文件格式,具體值爲0xCAFEBABE,
u2 minor_version; // Class文件格式副版本號,
u2 major_version; // Class文件格式主版本號,
u2 constant_pool_count; // 常量表項個數,
cp_info **constant_pool; // 常量表,又稱變長符號表,
u2 access_flags; // Class的聲明中使用的修飾符掩碼,
u2 this_class; // 常數表索引,索引內保存類名或接口名,
u2 super_class; // 常數表索引,索引內保存父類名,
u2 interfaces_count; // 超接口個數,
u2 *interfaces; // 常數表索引,各超接口名稱,
u2 fields_count; // 類的域個數,
field_info **fields; // 域數據,包括屬性名稱索引,
u2 methods_count; // 方法個數,
method_info **methods; // 方法數據,包括方法名稱索引,方法修飾符掩碼等,
u2 attributes_count; // 類附加屬性個數,
attribute_info **attributes; // 類附加屬性數據,包括源文件名等。
};
複製代碼
上面結構體的各個變量的定義的順序與Class文件中的存儲順序是一致的。下面咱們用一個二進制編輯器打開Class文件並簡單看下,Class文件中的存儲如何與上面的結構體對應的。數據結構
根據結構體的定義,首先是magic
字段,它是u4類型的,即4字節,對應於上圖中的CAFEBABE
;緊接着兩個u2類型的字段,minor_version
和major_version
,用來表示Class文件的版本號,對應於上圖中的00000031
;而後是一個u2類型的constant_pool_count
,用來表示常數表項個數,這裏是0027
,也就是有38個常量,所以常量從1開始計數的;接着常量個數的是變長符號表,這個符號表的長度就是以前常量的長度,即38,而後咱們要按照常量的規則找到38個常量以後就是u2類型的訪問標誌。併發
那這38個常量又如何尋找呢?實際上,Class文件中總計規定了14種常量結構體,每種結構體都包含一個tag
字段,它是u1類型的,即1個字節,而且存儲在該結構體的首位。咱們須要先根據該tag
字段找到該常量的定義,而後才能肯定接下來的幾個字節是屬於該常量的。好比上圖中的第一個常量的tag
是0A
,咱們能夠查詢相關的表知道它是CONSTANT_Fleddef_info
類型的常量,該常量有3個字段:u1類型的tag
, u2類型的index
, u2類型的index
,故咱們能夠肯定常量長度以後的5個字節是屬於第一個常量的。依次,分析下去咱們能夠獲得第二個,第三個等常量的信息。獲得了常量的信息以後,就能夠獲取上面結構體中其餘的字段的信息。編輯器
其實,按照上面的分析,咱們能夠看出分析Class文件時一個很是機械的過程,由於它有固定的規則在裏面,因此咱們可使用命令行工具javap
來獲取以上的信息。在實際開發過程當中從字節碼的角度分析問題的情形可能並很少,可是瞭解字節碼中的一些指令,尤爲是併發相關的指令,對學習和分析問題都大有裨益。工具
Java程序中的類加載是在運行期間完成的,咱們可使用Java預約義的加載器或者自定義加載器動態從各類渠道加載類並使用。最初將類加載器從虛擬機中分離出來是爲了Applet等的動態加載,可是後來隨着技術發展,動態加載被應用到各類場景中,好比移動端的插件化、熱補丁、Tomcat的類加載等各類場景中,因此類加載是很是重要的一塊內容。佈局
一個類從被加載到虛擬機內存到卸載的整個生命週期包括:加載-驗證-準備-解析-初始化-使用-卸載
7個階段。其中驗證-準備-解析
3個階段稱爲鏈接。學習
加載發生在類被使用的時候,若是一個類以前沒有被加載,那麼就會執行加載邏輯,好比當使用new建立類、調用靜態類對象和使用反射的時候等。加載過程主要工做包括:1).從磁盤或者網絡中獲取類的二進制字節流;2).將該字節流的靜態存儲結構轉換爲方法取的運行時數據結構;3).在內存中生成表示這個類的Class對象,做爲方法區訪問該類的各類數據結構的入口。ui
驗證階段會對加載的字節流中的信息進行各類校驗以確保它符合JVM的要求。
準備階段會正式爲類變量分配內存並設置類變量的初始值。注意這裏分配內存的只包括類變量,也就是靜態的變量(實例變量會在對象實例化的時候分配在堆上),而且這裏的設置初始值是指‘零值’,好比int類型的會被初始化爲0,引用類型的會被初始化爲null,即便你在代碼中爲其賦了值。
解析階段是將常量池中的符號引用替換爲直接引用的過程。符號引用與虛擬機實現的佈局無關,引用的目標並不必定要已經加載到內存中。各類虛擬機實現的內存佈局能夠各不相同,可是它們能接受的符號引用必須是一致的,只要能正肯定位到它們在內存中的位置就行。直接引用能夠是指向目標的指針,相對偏移量或是一個能間接定位到目標的句柄。若是有了直接引用,那引用的目標一定已經在內存中存在。
初始化是執行類構造器<client>
方法的過程。<client>
方法是由編譯器自動收集類中的類變量的賦值操做和靜態語句塊中的語句合併而成的。虛擬機會保證<client>
方法執行以前,父類的<client>
方法已經執行完畢。
類加載器用來根據類的全限定名獲取描述此類的二進制字節流。咱們能夠把類加載器分紅如下4種:
<Java_Home>/lib
下面的類庫加載到內存中(好比rt.jar);<Java_Home>/lib/ext
或者由系統變量 java.ext.dir
指定位置中的類庫加載到內存中;在Java種存在以上多種類加載器,它們之間經過必定的規則相互配合,這個規則就是雙親委派模型
:每一個類加載器收到類加載請求時,都會先將請求委派給父類加載器去完成,因此,加載請求會一直傳遞到最頂層的類加載器。只有當類父類加載器沒法完成加載請求的時候,該加載器纔會本身去加載。固然,這不是強制的,咱們也能夠徹底使用本身的一套邏輯。但雙清委派模型的好處就在於,假如你自定義了一個類java.lang.Object
,那麼當使用雙親委派模型來加載的時候,會由子加載器不斷向上傳遞加載請求到啓動類加載器進行加載,所以Object在各類類加載環境中都是同一個類。這保障了Java系統的穩定性。
虛擬機是相對於物理機而言的,它們的區別是物理機的執行引擎直接創建在處理器、硬件、指令集和操做系統層面上,而虛擬機的執行引擎是本身實現的。因此,執行引擎也是Java虛擬機最核心的組成部分之一。
執行引擎用來執行咱們寫的業務邏輯,而業務邏輯就是指一些方法,因此虛擬機執行引擎就是用來執行各個方法的。而方法是經過棧幀來描述的,方法的執行是用棧幀入棧和出棧來描述的,棧幀中存儲了方法的局部變量表、操做數棧、動態鏈接和方法返回地址等信息。因此說,執行引擎就是用來執行各個棧幀的。在虛擬機執行的時候,只有最頂層的棧幀是有效的,與之關聯的方法就稱爲當前方法,而且執行引擎運行的全部字節碼都是針對當前棧幀的。
方法的信息存儲在棧幀中,而棧幀中的方法信息是從Class文件中讀取來的。回到以前的Class類文件結構部分,每一個方法是經過結構體method_info
來描述的:
struct method_info
{
u2 access_flags; //方法修飾符掩碼
u2 name_index; //方法名在常數表內的索引
u2 descriptor_index; //方法描述符,其值是常數表內的索引
u2 attributes_count; //方法的屬性個數
attribute_info **attributes; //方法的屬性數據,
};
複製代碼
在method_info
中又包含了一個屬性表集合attribute_info
類型的attributes
,方法的局部變量表須要的空間大小和操做棧的深度等就記錄在其中。局部變量表用於存放方法參數和方法內的局部變量,當方法是實例方法的時候,局部變量表的第0位會被用來傳遞方法所屬對象的引用,即this
。Java虛擬機執行引擎是基於棧的執行引擎,這裏的棧就是操做數棧。操做數棧的深度也是記錄在方法屬性集合的Code屬性中的。
咱們能夠用下面的一個程序來講明如下在實際的執行過程當中,Java執行引擎是如何工做的。
public int cal() {
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c; // 1
}
複製代碼
首先會在編譯期肯定方法的棧深度和局部變量表的長度,棧深度是由計算的過程獲得的,而局部變量表的長度等於1個this
加3個局部變量即4。當程序執行到1處時,局部變量表內會被填充爲this
、100
、 200
和300
。程序計數器會隨着代碼當前執行到的位置而不斷更新。而此時由於沒有進行任何計算,因此棧仍是空的。
接下來就開始進行計算:首先會把a壓入棧中(其實時把局部變量表裏的值壓入棧中);而後把b壓入棧中;接着將棧頂的兩個元素先出棧,相加以後再入棧,此時棧中只有一個計算結果300;接下來再把c壓入棧中;而後,把棧頂的兩個元素出棧,執行完乘法以後再入棧,因此最終棧中只有一個90000;最後,使用ireturn
指令結束方法並將棧頂的元素返回給方法的調用者。
以上就是虛擬機執行子系統的一個過程,包含了從編譯出的Class文件,到被加載到內存中、驗證、初始化等,到最終在虛擬機中被執行等完整的過程。這裏只是總結和梳理了相關的基礎的知識點,在虛擬機中實際的執行過程確定遠比咱們上述的內容更加複雜和精彩。