深入理解Java虛擬機-類文件結構及加載

系列閱讀
1.深入理解Java虛擬機-GC&運行時數(shù)據區(qū)
2.深入理解Java虛擬機-類文件結構及加載
3.深入理解Java虛擬機-內存模型及多線程

1. JVM中立特性

  • 平臺無關性
    java宣傳口號為“一次編寫,到處運行”。各種不同平臺的虛擬機與所有平臺都統(tǒng)一使用的程序存儲格式-字節(jié)碼(ByteCode)是構成平臺無關性的基石。Class文件中包含了Java虛擬機指令集和符號表以及其他若干輔助信息。
  • 語言無關性
    運行在JVM上的語言:Java、Clojure、Groovy、JRuby、Jython、Scala。Java虛擬機只與"Class文件"這種特定的二進制文件格式所關聯(lián)。


    Java虛擬機提供的語言無關性

2. Class類文件

定義
JVM規(guī)范描繪了JVM應有的共同程序存儲格式:Class文件格式以及字節(jié)碼指令集。
Class文件是JVM執(zhí)行引擎的數(shù)據入口。任何一個Class文件都對應著惟一一個類或接口的定義信息。

結構
Class文件是一組以8位字節(jié)為基礎單位的二進制流,各項數(shù)據嚴格按順序排列,中間無分隔符。

Class文件格式采用類似C語言結構體的偽結構來存儲數(shù)據,包含兩種數(shù)據類型:

  • 無符號數(shù)
    屬于基本的數(shù)據類型,可描述數(shù)字、索引引用、數(shù)量值或者按照UTF-8編碼構成字符串值。

  • 由多個無符號數(shù)或其他表作為數(shù)據項構成的符合數(shù)據類型,描述有層次關系的復合結構的數(shù)據,整個Class文件本質上是一張表。

常量池
可理解為Class文件中的資源倉庫,主要存放兩大類常量:
字面量(Literal):接近Java語言的常量概念,如文本字符串、聲明為final的常量值等。
符號引用(Symbolic References):屬于編譯原理的概念,包含:

  • 類和接口的全限定名(Fully Qualified Name)
  • 字段的名稱和描述符(Descriptor)
  • 方法的名稱和描述符

常量池中每一項常量都是一個表。
當虛擬機運行時,從常量池獲得對應的符號引用,再在類創(chuàng)建時或運行時解析、翻譯到具體的內存地址之中。

描述符
用來描述字段的數(shù)據類型、方法的參數(shù)列表(包括數(shù)量、類型以及順序)和返回值。

描述符標識字符含義

對象頭
每個對象都有一個對象頭,存放對象的系統(tǒng)信息.包括對象的哈希值,年齡,鎖的指針等.

Code屬性
可以把一個Java程序的信息分為代碼(Code,方法體里的Java代碼)和元數(shù)據(Metadata,包括類、字段、方法定義及其他信息)兩部分。Code屬性用于描述代碼。
Java虛擬機執(zhí)行字節(jié)碼是基于棧的體系結構。虛擬機規(guī)范明確限制了一個方法不允許超過65535條字節(jié)碼指令,即2的16次方-1。
虛擬機運行時根據max_stack(操作數(shù)棧深度最大值)來分配棧幀中的操作棧深度。
在任何實例方法里面,都可以通過“this”關鍵字訪問到此方法所屬的對象,因此實例方法的局部變量表中至少會存在一個指向當前對象實例的局部變量。

ConstantValue屬性
屬性表中包含ContantValue屬性。對于非static類型的屬性(即實例變量)的賦值是在實例構造器<init>方法中進行的;對于類變量有兩種選擇:在類構造器<clinit>方法中或者使用ConstantValue屬性。

Sun Javac編譯器中,如果同時使用final和static修飾一個變量(常量),且數(shù)據類型是基本類型或Java.lang.String,就生成ConstantValue屬性進行初始化,如果這個變量沒有被final修飾,或者并非基本類型及字符串,則在<clinit>方法中進行初始化。

3. 字節(jié)碼指令集

定義
Java虛擬機的指令由一個字節(jié)長度的、代表著某種操作含義的數(shù)字(稱為操作碼Opcode)以及跟隨其后的零至多個代表此操作所需參數(shù)(稱為操作數(shù)Operands)而構成。
Java虛擬機規(guī)范已經定義了約200條編碼值對應的指令含義。指令含義-擴展
一條字節(jié)碼指令在解釋執(zhí)行時,解釋器將要運行需要行代碼才能實現(xiàn)它的語義;如果是編譯執(zhí)行,一條字節(jié)碼指令也可能轉化成若干條本地機器碼指令。
編譯型語言要先編譯再運行,而解釋性語言直接“運行”源代碼。java的編譯器先將其編譯為class文件,也就是字節(jié)碼;然后將字節(jié)碼交由jvm(java虛擬機)解釋執(zhí)行。參考

數(shù)據類型
編譯器會在編譯器或運行期將byte/short/boolean/char類型的數(shù)據/數(shù)組轉換成相應的int類型數(shù)據,并采用相應的int類型的字節(jié)碼指令來處理。

加載和存儲指令
用于將數(shù)據在棧幀中的局部變量表和操作數(shù)棧之間來回傳輸。

運算指令
用于對兩個操作數(shù)棧上的值進行某種特定運算,并將結果重新存入到操作棧頂。
JVM規(guī)范要求在處理浮點數(shù)時,嚴格遵循IEEE 754中定義的非正規(guī)浮點數(shù)值和逐級下溢的運算規(guī)則。

類型轉換指令
JVM直接支持(即轉換無需顯式的轉換指令)以下數(shù)值類型的寬化類型轉換:

  • int類型到long/float/double類型
  • long類型到float/double類型
  • float到double類型
    而窄化類型轉換則需要顯式使用輔助指令完成。可能會發(fā)生上限溢出、下限溢出和精度丟失等情況,但不可能拋出運行時異常。

4. 類加載機制

虛擬機把描述類的數(shù)據從Class文件加載到內存,并對數(shù)據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。

Java天生可以動態(tài)擴展的語言特性就是依賴運行期動態(tài)加載和動態(tài)連接這個特點實現(xiàn)的。

JVM加載類是一種懶加載的模式,只有在需要某個類的時候才會加載,只是聲明某個類的引用而不創(chuàng)建對象的時候是不會加載的。

類的生命周期

“解析”階段某些情況下可以在“初始化”階段之后開始,這是為了支持Java語言的運行時綁定(動態(tài)綁定/晚期綁定)。

數(shù)組類本身不通過類加載器創(chuàng)建,由JVM直接創(chuàng)建。

加載
加載階段需要完成3件事:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節(jié)流
  2. 將這個字節(jié)流所代表的靜態(tài)存儲結構轉化成方法區(qū)的運行時數(shù)據結構
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據的訪問入口。
    加載完成后,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中。

加載階段和連接階段的部分內容(如一部分字節(jié)碼文件格式驗證動作)是交叉進行的。

驗證
驗證是連接階段的第一步,目的是確保Class文件的字節(jié)流中包含的信息符合當前虛擬機的要求,且不會危害虛擬機自身的安全。主要完成以下4階段的檢驗動作。

  1. 文件格式檢驗
    驗證字節(jié)流是否符合Class文件格式的規(guī)范,且能被當前版本的虛擬機處理。
    主要目的是保證輸入的字節(jié)流能正確的解析并存儲于方法區(qū)之內,格式上符合描述一個Java類型信息的要求。
    只有通過這個階段的驗證,字節(jié)流才會進入內存的方法去中進行存儲。
  2. 元數(shù)據驗證
    第二階段是對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求。

元數(shù)據
1.定義
元數(shù)據是用來描述數(shù)據的數(shù)據。
在編程語言上下文中,元數(shù)據是添加到程序元素如方法、字段、類和包上的額外信息。
2.Java中的元數(shù)據
注解Annotation就是java平臺的元數(shù)據,是 J2SE5.0新增加的功能,該機制允許在Java 代碼中添加自定義注釋,并允許通過反射(reflection),以編程方式訪問元數(shù)據注釋。
常見作用為:生成文檔。跟蹤代碼依賴性,實現(xiàn)替代配置文件功能。在編譯時進行格式檢查。
3.特點
元數(shù)據以標簽的形式存在于Java代碼中。
元數(shù)據可以只存在于Java源代碼級別,也可以存在于編譯之后的Class文件內部。
元數(shù)據描述的信息是類型安全的,即元數(shù)據內部的字段都是有明確類型的。
元數(shù)據需要編譯器之外的工具額外的處理用來生成其它的程序部件。
參考

  1. 字節(jié)碼驗證
    對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事情。
    主要目的是通過數(shù)據流和控制流分析,確定程序語意是合法的、符合邏輯的。

  2. 符號引用驗證
    該階段的校驗發(fā)生在虛擬機將符號引用轉換為直接引用的時候,將在連接的第三個階段(解析階段)中發(fā)生。
    符號引用驗證可以看作是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗,確保解析動作能正常執(zhí)行。

準備
準備階段是正式為類變量分配內存并設置類變量初始值的階段,這些變量所使用的內存將在方法區(qū)中進行分配。
這時候進行內存分配的僅包括類變量(被static修飾的變量),通常情況下是數(shù)據類型的零值.。但是存在ConstantValue屬性的value會賦上指定的值。

基本數(shù)據類型的零值

解析
解析階段是虛擬機將常量池內的符號引用替換為直接引用的過程。

  1. 符號引用
    符號引用是一個字符串,它給出了被引用的內容的名字并且可能會包含一些其他關于這個被引用項的信息——這些信息必須足以唯一的識別一個類、字段、方法。
    例如,在Class文件中它以CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等類型的常量出現(xiàn)。
    符號引用與虛擬機實現(xiàn)的內存布局無關。引用的目標不一定已經加在到內存中。
  2. 直接引用
    直接指向目標的指針、相對偏移量、或間接定位到目標的句柄。
    對于指向“類型”(Class對象)、類變量、類方法的直接引用可能是指向方法區(qū)的本地指針。
    直接引用與虛擬機實現(xiàn)的內存布局相關。如果有直接引用,引用的目標必定已在內存中存在。
    參考

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符7類符號引用進行。

初始化
初始化階段才真正開始執(zhí)行類中定義的Java程序代碼(或字節(jié)碼)
初始化階段是執(zhí)行類構造器<clinit>()方法的過程,<clinic>()方法是由編譯器自動收集類中的所有類變量的復制動作和靜態(tài)語句塊(static{}塊)中的語句合并產生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序決定的。
<clinit>()方法不需要顯式地調用父類構造器,虛擬機會保證子類的<clinit>()方法執(zhí)行之前父類的<clinit>()方法已執(zhí)行完畢。
執(zhí)行接口的<clinit>()方法不需要先執(zhí)行父接口<clinit>()方法。

有且只有五種情況下必須立即對類進行“初始化”:
1) 遇到new、getstatic、putstatic、invokestatic四條指令時,如果類沒有進行過初始化,先觸發(fā)其初始化。生成這4條指令最常見的場景是:使用new關鍵詞實例化對象、讀取或設置一個類的靜態(tài)字段(final、已在編譯器把結果放入常量池的靜態(tài)字段除外)、調用一個類的靜態(tài)方法時。
2) 使用java.lang.reflect包的方法對類進行放射調用的時候,如沒進行過初始化則觸發(fā)。
3) 初始化一個類的時候,如果其父類沒有進行初始化,需先觸發(fā)父類初始化。
4) 當虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
5) 當使用JDK1.7的動態(tài)語言時支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,且句柄對應的類沒初始化過。
從Java虛擬機層面看,除了使用new關鍵字創(chuàng)建對象的方式外,其他方式全部都是通過轉變?yōu)閕nvokevirtual指令直接創(chuàng)建對象的。參考

初始化與實例化的區(qū)別
在同一個類加載器下,一個類型只會被初始化一次,但是可以任意實例化。
類的初始化是指類加載過程中的初始化階段對類變量按照程序員的意圖進行賦值的過程;而類的實例化是指在類完全加載到內存中后創(chuàng)建對象的過程。
實例化的觸發(fā)時機一般是new及反射調用。
類實例化的一般過程:
父類的類構造器<clinit>() -> 子類的類構造器<clinit>() -> 父類的成員變量和實例代碼塊 -> 父類的構造函數(shù) -> 子類的成員變量和實例代碼塊 -> 子類的構造函數(shù)。

類加載器
對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在JVM中的唯一性,每個類加載器都擁有一個獨立的類名稱空間。

啟動類加載器(Bootstrap ClassLoader)使用C++語言實現(xiàn),是虛擬機自身的一部分。
其他類加載器都由Java語言實現(xiàn),獨立于虛擬機外部。
應用程序類加載器是ClassLoader中的getSystemClassLoader()方法的返回值, 如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。

雙親委派模型工作流程:
如果一個類加載器收到了類加載的請求,首先不會自己去嘗試加載這個類,而是把請求委派給父類加載器去完成,每個層次的類加載都如此,因此所有的加載請求都傳送到頂層的類加載器中,只有當父加載器反饋無法完成這個加載請求時,子加載器才會嘗試自己去加載。


雙親委派模型

5. 虛擬機字節(jié)碼執(zhí)行

代碼編譯的結果是從本地機器碼轉變成字節(jié)碼。
虛擬機的執(zhí)行引擎是自己實現(xiàn)的,因此可自行制定指令集與執(zhí)行引擎的結構體系。

棧幀(Stack Frame)是用于支持虛擬機進行方法調用和方法執(zhí)行的數(shù)據結構,每個方法從調用開始至執(zhí)行完成的過程,都對應一個棧幀在虛擬機棧里面從入棧到出棧的過程。
在活動線程中,只有位于棧頂?shù)臈攀怯行У?,稱為當前棧幀。執(zhí)行引擎運行的所有字節(jié)碼指令都值針對當前棧幀進行操作。

棧幀的概念結構

注:主要內容摘錄自書籍 深入理解Java虛擬機,周志明 著

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。