Java虛擬機總結中篇

本篇博客主要針對 Java 虛擬機的類加載機制,虛擬機字節碼執行引擎,早期編譯優化進行總結,其餘部分總結請點擊 Java 虛擬總結上篇

一. 虛擬機類加載機制

概述

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

類加載的時機

類加載的時機不止一種:

  • 遇到 new 等字節碼指令時會進行類加載
  • 反射調用時會進行類加載

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

Student[] stus=new Student[10];

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

類加載的過程

加載過程

通過類的全限定名來獲取定義此類的二進制字節流,將這個字節流所代表的靜態存儲結構轉換爲方法區的運行時數據結構,在內存中生成一個代表類的數據訪問入口的 java.lang.Class 對象。

驗證過程

驗證過程的目的是爲了確保 Class 文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬機自身的安全。主要有

  • 文件格式驗證:驗證魔數,主次版本號,常量類型等。
  • 元數據驗證:是否有父類,是否繼承了不該繼承的類,抽象類是否實現了方法等。
  • 字節碼驗證:確保程序語義是合法的,符合邏輯的。如類型轉換,跳轉指令等。
  • 符號引用驗證:對類自身以外的信息(常量池中的各種引用)進行匹配校驗。

準備過程

正式爲類變量分配內存並設置類變量初始值的階段, 只包括類變量而不包括實例變量和 final 類變量,而且僅僅只是初始化爲 0 值。

解析過程

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

初始化階段

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

類加載器

實現通過一個類的全限定名獲取描述此類的二進制字節流的代碼模塊稱爲類加載器。比較兩個類是否相等,一定是在同一個類加載器的前提下進行的,否則哪怕 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 序列構造抽象語法樹的過程。抽象語法樹是用來描述程序代碼語法結構的樹形表示方法,每一個節點都代表着程序代碼的一個語法結構:包,類型,修飾符等。

註解處理器

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

語義分析

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

字節碼生成

將前面各個步驟生成的信息轉換成字節碼寫到磁盤中,類構造器 \ 和實例構造器 \ 就是在這個階段添加到語法樹中。

Java 語法糖

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

條件編譯

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

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