JVM學習筆記1.0

1.Java 虛擬機需要將字節碼翻譯成機器碼

    在 HotSpot 裏面,有兩種形式:第一種是解釋執行,即逐條將字節碼翻譯成機器碼並執行;第二種是即時編譯(Just-In-Time compilation,JIT),即將一個方法中包含的所有字節碼編譯成機器碼後再執行。前者的優勢在於無需等待編譯,而後者的優勢在於實際運行速度更快。HotSpot默認採用混合模式,綜合瞭解釋執行和即時編譯兩者的優點。它會先解釋執行字節碼,而後將其中反覆執行的熱點代碼,以方法爲單位進行即時編譯。

2.HotSpot的即時編譯器

    HotSpot 內置了多個即時編譯器:C1、C2 和 Graal。Graal 是 Java 10 正式引入的實驗性即時編譯器。之所以引入多個即時編譯器,是爲了在編譯時間和生成代碼的執行效率之間進行取捨。
    C1 又叫做 Client 編譯器,面向的是對啓動性能有要求的客戶端 GUI 程序,採用的優化手段相對簡單,因此編譯時間較短。C2 又叫做 Server 編譯器,面向的是對峯值性能有要求的服務器端程序,採用的優化手段相對複雜,因此編譯時間較長,但同時生成代碼的執行效率較高。
    從 Java 7 開始,HotSpot 默認採用分層編譯的方式:熱點方法首先會被 C1 編譯,而後熱點方法中的熱點會進一步被 C2 編譯。爲了不干擾應用的正常運行,HotSpot 的即時編譯是放在額外的編譯線程中進行的。HotSpot 會根據 CPU 的數量設置編譯線程的數目,並且按 1:2 的比例配置給 C1 及 C2 編譯器。在計算資源充足的情況下,字節碼的解釋執行和即時編譯可同時進行。編譯完成後的機器碼會在下次調用該方法時啓用,以替換原本的解釋執行。

3.類的初始化

類的初始化何時會被觸發呢?JVM 規範枚舉了下述多種觸發情況:
    1.當虛擬機啓動時,初始化用戶指定的主類;
    2.當遇到用以新建目標類實例的 new 指令時,初始化 new 指令的目標類;
    3.當遇到調用靜態方法的指令時,初始化該靜態方法所在的類;當遇到訪問靜態字段的指令時,初始化該靜態字段所在的類;
    4.子類的初始化會觸發父類的初始化;如果一個接口定義了 default 方法,那麼直接實現或者間接實現該接口的類的初始化,會觸發該接口的初始化;
    5.使用反射 API 對某個類進行反射調用時,初始化這個類;當初次調用 MethodHandle 實例時,初始化該 MethodHandle 指向的方法所在的類。

4.異常處理

1.異常處理的兩大組成要素是拋出異常捕獲異常。這兩大要素共同實現程序控制流的非正常轉移。

    拋出異常可分爲顯式和隱式兩種。顯式拋異常的主體是應用程序,它指的是在程序中使用“throw”關鍵字,手動將異常實例拋出。
    隱式拋異常的主體則是 Java 虛擬機,它指的是 Java 虛擬機在執行過程中,碰到無法繼續執行的異常狀態,自動拋出異常。舉例來說,Java 虛擬機在執行讀取數組操作時,發現輸入的索引值是負數,故而拋出數組索引越界異常(ArrayIndexOutOfBoundsException)。

2.捕獲異常則涉及瞭如下三種代碼塊:

    try 代碼塊:用來標記需要進行異常監控的代碼。

    catch 代碼塊:跟在 try 代碼塊之後,用來捕獲在 try 代碼塊中觸發的某種指定類型的異常。除了聲明所捕獲異常的類型之外,catch 代碼塊還定義了針對該異常類型的異常處理器。在 Java 中,try 代碼塊後面可以跟着多個 catch 代碼塊,來捕獲不同類型的異常。Java 虛擬機會從上至下匹配異常處理器。因此,前面的 catch 代碼塊所捕獲的異常類型不能覆蓋後邊的,否則編譯器會報錯。

    finally 代碼塊:跟在 try 代碼塊和 catch 代碼塊之後,用來聲明一段必定運行的代碼。它的設計初衷是爲了避免跳過某些關鍵的清理代碼,例如關閉已打開的系統資源。

3.在 Java 語言規範中,所有異常都是 Throwable 類或者其子類的實例。Throwable 有兩大直接子類。第一個是 Error,涵蓋程序不應捕獲的異常。當程序觸發 Error 時,它的執行狀態已經無法恢復,需要中止線程甚至是中止虛擬機。第二子類則是 Exception,涵蓋程序可能需要捕獲並且處理的異常。

4.RuntimeException 和 Error 屬於 Java 裏的非檢查異常(unchecked exception)。其他異常則屬於檢查異常(checked exception)。在 Java 語法中,所有的檢查異常都需要程序顯式地捕獲,或者在方法聲明中用 throws 關鍵字標註。通常情況下,程序中自定義的異常應爲檢查異常,以便最大化利用 Java 編譯器的編譯時檢查。

5.Java 字節碼中,每個方法對應一個異常表。當程序觸發異常時,Java 虛擬機將查找異常表,並依此決定需要將控制流轉移至哪個異常處理器之中。Java 代碼中的 catch 代碼塊和 finally 代碼塊都會生成異常表條目。

5.反射

    Method.invoke 實際上委派給 MethodAccessor 來處理。MethodAccessor 是一個接口,它有兩個已有的具體實現:一個通過本地方法來實現反射調用,另一個則使用了委派模式。
    Java 的反射調用機制還設立了另一種動態生成字節碼的實現(下稱動態實現),直接使用 invoke 指令來調用目標方法。之所以採用委派實現,便是爲了能夠在本地實現以及動態實現中切換。
    動態實現和本地實現相比,其運行效率要更快。這是因爲動態實現無需經過 Java 到 C++ 再到 Java 的切換,但由於生成字節碼十分耗時,僅調用一次的話,反而是本地實現更快。
    考慮到許多反射調用僅會執行一次,Java 虛擬機設置了一個閾值 15(可以通過 -Dsun.reflect.inflationThreshold= 來調整),當某個反射調用的調用次數在 15 之下時,採用本地實現;當達到 15 時,便開始動態生成字節碼,並將委派實現的委派對象切換至動態實現,這個過程稱之爲 Inflation。
    方法的反射調用會帶來不少性能開銷,原因主要有三個:變長參數方法導致的 Object 數組,基本類型的自動裝箱、拆箱,還有最重要的方法內聯。

6.Java 虛擬機中 synchronized 關鍵字的實現

    Java 虛擬機中 synchronized 關鍵字的實現,按照代價由高至低可分爲重量級鎖、輕量級鎖和偏向鎖三種。
    重量級鎖會阻塞、喚醒請求加鎖的線程。它針對的是多個線程同時競爭同一把鎖的情況。爲了儘量避免昂貴的線程阻塞、喚醒操作,Java 虛擬機會在線程進入阻塞狀態之前,以及被喚醒後競爭不到鎖的情況下,進入自旋狀態,在處理器上空跑並且輪詢鎖是否被釋放。如果此時鎖恰好被釋放了,那麼當前線程便無須進入阻塞狀態,而是直接獲得這把鎖。Java 虛擬機採取了自適應自旋(根據以往自旋等待時是否能夠獲得鎖,來動態調整自旋的時間(循環數目)),來避免線程在面對非常小的 synchronized 代碼塊時,仍會被阻塞、喚醒的情況。
    輕量級鎖採用 CAS 操作,將鎖對象的標記字段替換爲一個指針,指向當前線程棧上的一塊空間,存儲着鎖對象原本的標記字段。它針對的是多個線程在不同時間段申請同一把鎖的情況。
    偏向鎖只會在第一次請求時採用 CAS 操作,在鎖對象的標記字段中記錄下當前線程的地址。在之後的運行過程中,持有該偏向鎖的線程的加鎖操作將直接返回。它針對的是鎖僅會被同一線程持有的情況。

7.即時編譯

    即時編譯是一項用來提升應用程序運行效率的技術。通常而言,代碼會先被 Java 虛擬機解釋執行,之後反覆執行的熱點代碼則會被即時編譯成爲機器碼,直接運行在底層硬件之上。
    對於執行時間較短的,或者對啓動性能有要求的程序,採用編譯效率較快的 C1;。對於執行時間較長的,或者對峯值性能有要求的程序,採用生成代碼執行效率較快的 C2。
    Java 虛擬機是根據方法的調用次數以及循環回邊的執行次數(在字節碼中,可以簡化理解爲往回跳轉的指令)來觸發即時編譯的。

8.方法內聯

    方法內聯指的是:在編譯過程中遇到方法調用時,將目標方法的方法體納入編譯範圍之中,並取代原方法調用的優化手段。
    在 C2 編譯器中,方法內聯是在解析字節碼的過程中完成的。每當碰到方法調用字節碼時,C2 將決定是否需要內聯該方法調用。如果需要內聯,則開始解析目標方法的字節碼。
    方法內聯能夠觸發更多的優化。通常而言,內聯越多,生成代碼的執行效率越高。然而,對於即時編譯器來說,內聯越多,編譯時間也就越長,而程序達到峯值性能的時刻也將被推遲。此外,內聯越多也將導致生成的機器碼越長。在 Java 虛擬機裏,編譯生成的機器碼會被部署到 Code Cache 之中。這個 Code Cache 是有大小限制的(由 Java 虛擬機參數 -XX:ReservedCodeCacheSize 控制)。這就意味着,生成的機器碼越長,越容易填滿 Code Cache,從而出現 Code Cache 已滿,即時編譯已被關閉的警告信息(CodeCache is full. Compiler has been disabled)。因此,即時編譯器不會無限制地進行方法內聯。

    內聯規則:

  • 由 -XX:CompileCommand 中的 inline 指令指定的方法,以及由 @ForceInline 註解的方法(僅限於 JDK 內部方法),會被強制內聯。
  • 如果調用字節碼對應的符號引用未被解析、目標方法所在的類未被初始化,或者目標方法是 native 方法,都將導致方法調用無法內聯。
  • C2 不支持內聯超過 9 層的調用(可以通過虛擬機參數 -XX:MaxInlineLevel 調整),以及 1 層的直接遞歸調用(可以通過虛擬機參數 -XX:MaxRecursiveInlineLevel 調整)。

    對於需要動態綁定的虛方法調用來說,即時編譯器則需要先對虛方法調用進行去虛化(devirtualize),即轉換爲一個或多個直接調用,然後才能進行方法內聯。

  • 完全去虛化通過類型推導或者類層次分析,將虛方法調用轉換爲直接調用。它的關鍵在於證明虛方法調用的目標方法是唯一的。
  • 條件去虛化通過向代碼中增添類型比較,將虛方法調用轉換爲一個個的類型測試以及對應該類型的直接調用。它將藉助 Java 虛擬機所收集的類型 Profile。

9.JNI

    Java 虛擬機會將所有 JNI 函數的函數指針聚合到一個名爲JNIEnv的數據結構之中。這是一個線程私有的數據結構。Java 虛擬機會爲每個線程創建一個JNIEnv,並規定 C 代碼不能將當前線程的JNIEnv共享給其他線程,否則 JNI 函數的正確性將無法保證。
    JNI 中的引用可分爲局部引用和全局引用。這兩者都可以阻止垃圾回收器回收被引用的 Java 對象。不同的是,局部引用在 native 方法調用返回之後便會失效。傳入參數以及大部分 JNI API 函數的返回值都屬於局部引用。

10元註解

  • @Target註解
    Target註解的作用是:描述註解的使用範圍(即:被修飾的註解可以用在什麼地方) 。
  • @Retention註解
    Reteniton註解的作用是:描述註解保留的時間範圍(即:被描述的註解在它所修飾的類中可以被保留到何時) 。
  • @Documented註解
    Documented註解的作用是:描述在使用 javadoc 工具爲類生成幫助文檔時是否要保留其註解信息。
  • @Inherited註解
    Inherited註解的作用是:使被它修飾的註解具有繼承性(如果某個類使用了被@Inherited修飾的註解,則其子類將自動具有該註解)。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章