Java虛擬機(二)執行子系統

(二)執行子系統

1.class類文件結構

Class文件是一組以8位字節爲基礎單位的二進制流,各個數據項目嚴格按照順序緊湊地排列在Class文件之中,中間沒有添加任何分隔符,這使得整個Class文件中存儲的內容幾乎全部是程序運行的必要數據,沒有空隙存在。當遇到需要佔用8位字節以上空間的數據項時,則會按照高位在前的方式分割成若干個8位字節進行存儲。

Java虛擬機規範描繪了Java虛擬機應有的共同程序存儲格式:Class文件格式以及字節碼指令集。這些內容與硬件、操作系統及具體的Java虛擬機實現之間是完全獨立的,虛擬機實現者可能更願意把它們看做是程序在各種Java平臺實現之間互相安全地交互的手段。

虛擬機實現者可以優化Class文件,但只要能滿足Class文件的格式標準,所以可以使用這種伸縮性來讓Java虛擬機獲得更高的性能、更低的內存消耗或者更好的可移植性,選擇哪種特性取決於Java虛擬機實現的目標和關注點是什麼。虛擬機實現的方式主要有以下兩種:
將輸入的Java虛擬機代碼在加載或執行時翻譯成另外一種虛擬機的指令集。
將輸入的Java虛擬機代碼在加載或執行時翻譯成宿主機CPU的本地指令集(即JIT代碼生成技術)。

Class文件格式採用一種僞結構來存儲數據,這種僞結構中只有兩種數據類型:無符號數和表,後面的解析都要以這兩種數據類型爲基礎,所以這裏要先介紹這兩個概念。

  • 無符號數
    無符號數屬於基本的數據類型,以u1、u2、u4、u8來分別代表1個字節、2個字節、4個字節和8個字節的無符號數,無符號數可以用來描述數字、索引引用、數量值或者按照UTF-8編碼構成字符串值。

  • 表是由多個無符號數或者其他表作爲數據項構成的複合數據類型,所有表都習慣性地以”_info”結尾。表用於描述有層次關係的複合結構的數據,整個Class文件本質上就是一張表,它由表6-1所示的數據項構成。
    Class文件表
    無論是無符號數還是表,當需要描述同一類型但數量不定的多個數據時,經常會使用一個前置的容量計數器加若干個連續的數據項的形式,這時稱這一系列連續的某一類型的數據爲某一類型的集合。

下面解析Class文件
1.魔數
每個Class文件的頭4個字節稱爲魔數(Magic Number),它的唯一作用是確定這個文件是否爲一個能被虛擬機接受的Class文件。Class文件的魔數是“0xCAFEBABE”
2.版本號
緊接着魔數的4個字節存儲的是Class文件的版本號:第5和第6個字節是次版本號(Minor Version),第7和第8個字節是主版本號(Major Version)。
3.常量池

  • 首先是:常量池容量(記錄常量的數量)
    由於常量池中常量的數量是不固定的,所以在常量池的入口需要放置一項u2類型的數據,代表常量池容量計數值(constant_pool_count)。
    這個容量計數是從1而不是0開始的,在Class文件格式規範制定之時,設計者將第0項常量空出來是有特殊考慮的,這樣做的目的在於滿足後面某些指向常量池的索引值的數據在特定情況下需要表達“不引用任何一個常量池項目”的含義,這種情況就可以把索引值置爲0來表示。
  • 然後是:依次描述每個常量,每個常量都是一個表
    • 字面量
      字面量比較接近於Java語言層面的常量概念,如文本字符串、聲明爲final的常量值等。
    • 符號引用
      符號引用則屬於編譯原理方面的概念,包括了下面三類常量:
      • 類和接口的全限定名(Fully Qualified Name)
      • 字段的名稱和描述符(Descriptor)
      • 方法的名稱和描述符

14種常量類型
14種常量類型

每種常量類型都有自己的表結構

比如 CONSTANT_Class_info 和 CONSTANT_Utf8_info
這裏寫圖片描述
這裏寫圖片描述

4.訪問標誌
在所有的常量列出後,緊接着的兩個字節代表訪問標誌(access_flags),這個標誌用於識別一些類或者接口層次的訪問信息,包括:這個Class是類還是接口;是否定義爲public類型;是否定義爲abstract類型;如果是類的話,是否被聲明爲final等。

5.類索引、父類索引與接口索引集合
類索引用於確定這個類的全限定名,父類索引用於確定這個類的父類的全限定名。
類索引和父類索引用兩個u2類型的索引值表示,它們各自指向一個類型爲CONSTANT_Class_info的類描述符常量,通過CONSTANT_Class_info類型的常量中的索引值可以找到定義在CONSTANT_Utf8_info類型的常量中的全限定名字符串。
對於接口索引集合,入口的第一項——u2類型的數據爲接口計數器(interfaces_count),表示索引表的容量。如果該類沒有實現任何接口,則該計數器值爲0,後面接口的索引表不再佔用任何字節。

6.字段表集合
字段表(field_info)用於描述接口或者類中聲明的變量。字段(field)包括類級變量以及實例級變量,但不包括在方法內部聲明的局部變量。
可以包括的信息有:字段的作用域(public、private、protected修飾符)、是實例變量還是類變量(static修飾符)、可變性(final)、併發可見性(volatile修飾符,是否強制從主內存讀寫)、可否被序列化(transient修飾符)、字段數據類型(基本類型、對象、數組)、字段名稱。

access_flags
字段修飾符放在access_flags項目中,它與類中的access_flags項目是非常類似的,都是一個u2的數據類型,其中可以設置的標誌位和含義見表6-9。

name_index和descriptor_index
對常量池的引用,分別代表着字段的簡單名稱以及字段和方法的描述符。
attributes_count和attributes
是跟着的屬性表

7.方法表集合

此處是定義方法。
方法裏的Java代碼,經過編譯器編譯成字節碼指令後,存放在方法屬性表集合中一個名爲”Code”的屬性裏面

8.屬性表集合
在Class文件、字段表、方法表都可以攜帶自己的屬性表集合,以用於描述某些場景專有的信息。
爲了能正確解析Class文件,預定義了虛擬機實現應當能識別的屬性,每個屬性都自己的名稱。使用位置。
下面是部分屬性

對於每個屬性,它的名稱需要從常量池中引用一個CONSTANT_Utf8_info類型的常量來表示,而屬性值的結構則是完全自定義的,只需要通過一個u4的長度屬性去說明屬性值所佔用的位數即可。一個符合規則的屬性表應該滿足表6-14中所定義的結構。

舉幾個屬性表例子:
1.Code屬性表

attribute_name_index是一項指向CONSTANT_Utf8_info型常量的索引,常量值固定爲”Code”,它代表了該屬性的屬性名稱,attribute_length指示了屬性值的長度,由於屬性名稱索引與屬性長度一共爲6字節,所以屬性值的長度固定爲整個屬性表長度減去6個字節。
max_stack代表了操作數棧(Operand Stacks)深度的最大值。在方法執行的任意時刻,操作數棧都不會超過這個深度。虛擬機運行的時候需要根據這個值來分配棧幀(Stack Frame)中的操作棧深度。
max_locals代表了局部變量表所需的存儲空間。
code_length和code用來存儲Java源程序編譯後生成的字節碼指令。code_length代表字節碼長度,code是用於存儲字節碼指令的一系列字節流。
2.Exceptions屬性表
3.LineNumberTable屬性
4.LocalVariableTable屬性
5.SourceFile屬性
6.ConstantValue屬性
7.InnerClasses屬性
8.Deprecated及Synthetic屬性
9.StackMapTable屬性
10.Signature屬性
11.BootstrapMethods屬性

2.類加載機制

(虛擬機如何加載這些Class文件?
Class文件中的信息進入到虛擬機後會發生什麼變化?)

類加載的時機
類的生命週期

虛擬機規範沒有對什麼時候進行加載做明確規定,但是明確規定了什麼時候進行初始化(而加載、驗證、準備自然需要在此之前開始),有且只有五中情況:
1)遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
2)使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
4)當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類
5)當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

這5種場景中的行爲稱爲對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱爲被動引用。

接口的加載過程與類加載過程稍有一些不同,針對接口需要做一些特殊說明:接口也有初始化過程,這點與類是一致的,上面的代碼都是用靜態語句塊”static{}”來輸出初始化信息的,而接口中不能使用”static{}”語句塊,但編譯器仍然會爲接口生成”<clinit>()”類構造器,用於初始化接口中所定義的成員變量。接口與類真正有所區別的是前面講述的5種“有且僅有”需要開始初始化場景中的第3種:當一個類在初始化時,要求其父類全部都已經初始化過了,但是一個接口在初始化時,並不要求其父接口全部都完成了初始化,只有在真正使用到父接口的時候(如引用接口中定義的常量)纔會初始化。

類加載的過程
1.加載
在加載階段,虛擬機需要完成以下3件事情:
1)通過一個類的全限定名來獲取定義此類的二進制字節流。
2)將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構。
3)在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口。

相對於類加載過程的其他階段,一個非數組類的加載階段(準確地說,是加載階段中獲取類的二進制字節流的動作)是開發人員可控性最強的,因爲加載階段既可以使用系統提供的引導類加載器來完成,也可以由用戶自定義的類加載器去完成,開發人員可以通過定義自己的類加載器去控制字節流的獲取方式(即重寫一個類加載器的loadClass()方法)。
對於數組類而言,情況就有所不同,數組類本身不通過類加載器創建,它是由Java虛擬機直接創建的。但數組類與類加載器仍然有很密切的關係,因爲數組類的元素類型(Element Type,指的是數組去掉所有維度的類型)最終是要靠類加載器去創建。

2.驗證

  • 文件格式驗證
    驗證字節流是否符合Class文件格式的規範,並且能被當前版本的虛擬機處理。
  • 元數據驗證
    對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求
  • 字節碼驗證
    整個驗證過程中最複雜的一個階段,主要目的是通過數據流和控制流分析,確定程序語義是合法的、符合邏輯的。在第二階段對元數據信息中的數據類型做完校驗後,這個階段將對類的方法體進行校驗分析,保證被校驗類的方法在運行時不會做出危害虛擬機安全的事件,
  • 符號引用驗證
    最後一個階段的校驗發生在虛擬機將符號引用轉化爲直接引用的時候,這個轉化動作將在連接的第三階段——解析階段中發生。符號引用驗證可以看做是對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗

3準備.
式爲類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法區中進行分配。

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

5.初始化
在準備階段,變量已經賦過一次系統要求的初始值,而在初始化階段,則根據程序員通過程序制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執行類構造器<clinit>()方法的過程。

類加載器
雙親委派模型:

雙親委派模型的工作過程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋自己無法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試自己去加載。
啓動類加載器(Bootstrap ClassLoader),這個類加載器使用C++語言實現,是虛擬機自身的一部分;另一種就是所有其他的類加載器,這些類加載器都由Java語言實現,獨立於虛擬機外部,並且全都繼承自抽象類java.lang.ClassLoader。

3.字節碼執行

執行引擎是Java虛擬機最核心的組成部分之一。“虛擬機”是一個相對“物理機”的概念,這兩種機器都有代碼執行能力,其區別是物理機的執行引擎是直接建立在處理器、硬件、指令集和操作系統層面上的,而虛擬機的執行引擎則是由自己實現的,因此可以自行制定指令集與執行引擎的結構體系,並且能夠執行那些不被硬件直接支持的指令集格式。

執行引擎在執行Java代碼的時候可能會有解釋執行(通過解釋器執行)和編譯執行(通過即時編譯器產生本地代碼執行)兩種選擇,也可能兩者兼備,甚至還可能會包含幾個不同級別的編譯器執行引擎。但從外觀上看起來,所有的Java虛擬機的執行引擎都是一致的:輸入的是字節碼文件,處理過程是字節碼解析的等效過程,輸出的是執行結果

基於棧的執行引擎:(待完善)
在運行時,棧的結構以及方法調用的分派問題。

1.棧幀結構

2.方法調用

執行引擎在解釋執行時的過程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章