Java 虛擬機器總結給面試的你(中)

Hugo_Gao發表於2018-02-06

本篇部落格主要針對Java虛擬機器的類載入機制,虛擬機器位元組碼執行引擎,早期編譯優化進行總結,其餘部分總結請點選Java虛擬總結上篇

一.虛擬機器類載入機制

概述

虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這就是虛擬機器的類載入機制。

類載入的時機

類載入的時機不止一種:

  • 遇到new等位元組碼指令時會進行類載入
  • 反射呼叫時會進行類載入

在初始化時,若待初始化的類有父類則其父類先進行初始化(介面除外),並且先初始化包含main的主類。需要注意的是子類引用父類非final靜態變數時,只初始化靜態變數所在類,即父類,而引用final型別static變數不會引起任何初始化,因為其編譯期間就已經儲存在常量池中了。另外陣列定義也是不會引發類的初始化。比如

Student[] stus=new Student[10];
複製程式碼

是不會引起Student類的初始化的。

類載入的過程

載入過程

通過類的全限定名來獲取定義此類的二進位制位元組流,將這個位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構,在記憶體中生成一個代表類的資料訪問入口的java.lang.Class物件。

驗證過程

驗證過程的目的是為了確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。主要有

  • 檔案格式驗證:驗證魔數,主次版本號,常量型別等。
  • 後設資料驗證:是否有父類,是否繼承了不該繼承的類,抽象類是否實現了方法等。
  • 位元組碼驗證:確保程式語義是合法的,符合邏輯的。如型別轉換,跳轉指令等。
  • 符號引用驗證:對類自身以外的資訊(常量池中的各種引用)進行匹配校驗。

準備過程

正式為類變數分配記憶體並設定類變數初始值的階段,只包括類變數而不包括例項變數和final類變數,而且僅僅只是初始化為0值。

解析過程

虛擬機器將常量池內的符號引用轉換為直接引用的過程。符號引用用一組符號來描述所引用的目標。而直接引用是直接指向目標的指標,相對偏移量或是一個能間接定位到目標的控制程式碼。

初始化階段

在初始化階段真正開始執行Java程式程式碼(位元組碼),執行類的構造器<clinit>()方法,<clinit>()方法是由編譯器自動收集所有類變數的賦值動作和靜態語句塊的語句合併而成,同一類中的靜態塊與類變數按順序初始化,在同一個載入器下,一個類只會被初始化一次。

類載入器

實現通過一個類的全限定名獲取描述此類的二進位制位元組流的程式碼模組稱為類載入器。比較兩個類是否相等,一定是在同一個類載入器的前提下進行的,否則哪怕Class檔案都一樣也不相等

類載入器的分類

  • 啟動類載入器, 負責將存放在<JAVA_HOME>\lib目錄或-Xbootclasspath引數指定的路徑中的類庫載入到記憶體中。
  • 擴充套件類載入器,負責載入<JAVA_HOME>\lib\ext目錄或java.ext.dirs系統變數指定的路徑中的所有類庫。
  • 應用程式類載入器,負責載入使用者類路徑(classpath)上的指定類庫,我們可以直接使用這個類載入器。一般情況,如果我們沒有自定義類載入器預設就是用這個載入器。

雙親委派模型

雙親委派模型

雙親委派模型工作過程是:如果一個類載入器收到類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器完成。每個類載入器都是如此,只有當父載入器在自己的搜尋範圍內找不到指定的類時(即ClassNotFoundException),子載入器才會嘗試自己去載入。

這樣做的好處是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。

二.虛擬機器位元組碼執行引擎

虛擬機器的執行引擎自行實現,可以自行制定指令集與執行引擎的結構體系。

棧幀

棧幀是用於支援虛擬機器進行方法呼叫和方法執行的資料結構,是虛擬機器棧的棧元素。它儲存了方法的區域性變數表,運算元棧,動態連結,方法返回地址,對於活動執行緒來說,只有棧頂的棧幀才是有效的,稱為當前棧幀,與其關聯的方法叫做當前方法。

區域性變數表

區域性變數表存放方法引數和方法內部定義的變數。單位是slot(槽),最大可以達到32位。垃圾回收時,slot可以複用,將不使用的變數置為null是有意義的,方便垃圾回收。區域性變數不像類變數,是沒有初始值的。

JIT編譯器

當虛擬機器發現某個方法或程式碼塊執行特別頻繁時,就會把這些程式碼認定為 “Hot Spot Code”(熱點程式碼),為了提高熱點程式碼的執行效率,在執行時,虛擬機器將會把這些程式碼編譯成與本地平臺相關的機器碼,並進行各層次的優化,完成這項任務的正是 JIT 編譯器。

方法返回地址

  • 遇到方法的返回指令-->正常完成出口
  • 遇到異常並且未處理-->異常完成出口,不會給上層呼叫者產生任何返回值

方法呼叫

方法在編譯時並不確定方法的真實地址,而是一個符號引用,使得Java的動態擴充套件能力提升,在類載入過程甚至執行時才確定目標方法的直接引用。

解析

在類的解析階段將一部分符號引用轉換為直接引用,這部分符號引用代表的方法必須“編譯期可知,執行時不變”,如靜態方法,私有方法,例項構造器,父類方法。final方法也是。

分派

靜態分派(與過載相關),依賴靜態型別來定位方法執行版本的分派動作。自動轉型順序:char->int->long->float->double->Character->Serializable->Object->char...

動態分派(重寫相關),找到運算元棧頂的第一個元素所指向的物件的實際型別,若常量池中的描述符和簡單名稱都相符,則返回直接引用,否則對其父類進行第二步。

動態分配的實現:

動態分配的實現

在類的方法區建立一個虛方法表提升效率,若子類未重寫父類的方法,則子類的繼承方法中地址和父類方法的地址是一樣的,若重寫了父類的方法,則子類的方法地址就會改變,指向自己實現的版本。如上圖Son的clone方法沒有被重寫,指向的是Object父類的地址,而hardChoice方法被重寫了,指向的是Son自己實現的地址。

動態型別語言

型別檢查的主題過程在執行期而不是在編譯期,如Python,Javascript,Ruby,PHP,與之相對的就是靜態語言。

解釋執行與編譯執行

解釋執行為邊解釋邊執行,編譯執行則是先將原始碼編譯成目標語言 (如: 機器語言) 之後通過連線程式連線到生成的目標程式進行執行。

基於棧的位元組碼解釋執行引擎

  • 基於棧的指令集:Java編譯器輸出的指令流
  • 基於暫存器的指令集:x86彙編

三.早期編譯器優化

編譯器

三種編譯器:

  • 前端編譯器:把*.java變成*.class的過程,eg:Javac
  • 後端執行期編譯器(JIT):把位元組碼變成機器碼的過程,eg:Hotpot的C1,C2編譯器
  • 靜態提前編譯器(AOT):直接把*.java變成機器碼的過程,eg:GCJ(GNU Compiler for the Java)

解析與填充符號表

詞法分析

標記是編譯過程的最小元素,關鍵字、變數名、字面量、運算子都可以成為標記,詞法分析就是將原始碼的字元流轉變為標記集合。

語法分析

語法分析是根據Token序列構造抽象語法樹的過程。抽象語法樹是用來描述程式程式碼語法結構的樹形表示方法,每一個節點都代表著程式程式碼的一個語法結構:包,型別,修飾符等。

註解處理器

類似編譯器的一種外掛,如果外掛對語法樹進行了修改,編譯器將回到解析及填充符號表的過程重新處理。

語義分析

對語法抽象樹進行上下文有關性質的審查,如型別檢查。

位元組碼生成

將前面各個步驟生成的資訊轉換成位元組碼寫到磁碟中,類構造器<cinit>和例項構造器<init>就是在這個階段新增到語法樹中。

Java語法糖

  • 泛型與型別擦除:與C#不一樣,Java的泛型是偽泛型,在生成的位元組碼中已經被替換成了原生型別了,會自動加上型別轉換。
  • 遍歷:自動轉換為iterator遍歷。
  • 裝箱與拆箱:==運算在不遇到算數運算的情況下不會自動拆箱。equals方法不會處理資料的型別轉換,而==會。

條件編譯

編譯器不會編譯if到達不到的語句,也就是取消分支不成立的程式碼塊,可以檢視反編譯後的程式碼驗證條件編譯。

相關文章