JVM類載入機制小結

今天你做題了嗎發表於2020-07-04

  這篇文章我們關注一個問題:Java程式是怎麼進入JVM並執行的?經常寫Java程式的小夥伴應該都聽說過類載入機制,在《深入理解Java虛擬機器》裡周老師已經講的很清楚了,這篇隨筆把之前的筆記以及一些總結重新梳理一下。前面我們已經知道 .java檔案經過編譯後變成Class檔案,JVM載入的是位元組碼檔案。這其中的細節不知道小夥伴們有麼有了解過?

   類從被JVM載入到記憶體開始,到解除安裝出記憶體為止,整個生命週期包括7個階段,載入、驗證、準備、初始化、解除安裝這個5個步驟順序是確定的。

  • 載入,類的載入時機由JVM自行決定,JVM需要完成3件事情:
    • 通過類的全限定名獲取類的二進位制位元組流
    • 將類的靜態儲存結構轉化為方法區的執行時資料結構
    • 在記憶體中生成類的Class物件,作為方法區資料的入口

  陣列在Java中是一種引用型別,陣列類的載入由Java虛擬機器直接建立。類載入完成後,二進位制位元組流儲存在方法區中,HotSpot虛擬機器會建立一個java.lang.Class物件作為程式訪問方法區中的資料的外部介面,這個Class物件儲存在方法區中,載入階段何連線部分階段是交叉進行的。

  • 驗證是連線部分的第一步,驗證階段是確保Class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身的安全。因為只有在位元組碼層面才能保證Java語言的相對安全性。驗證階段會大致完成4個階段的檢驗動作:
    • 檔案格式驗證:驗證位元組流是否符合Class檔案格式的規範,並且能被當前版本的JVM處理。只有驗證通過了,位元組流才會進入方法區儲存。
    • 後設資料驗證:比如類是否有父類,類是否繼承了不能被繼承的類等,保證不存在不符合Java語言規範的後設資料資訊。
    • 位元組碼驗證:對類的方法體進行校驗分析
    • 符號引用驗證:對常量池中的各種符號引用進行校驗
  • 準備:為類變數分匹記憶體並設定型別變數的零值階段。類變數指的是被static修飾的變數,不包括例項變數。例如變數i,在準備階段值就是0L,只有在執行方法的時候值才會變成1。
private static long a = 1;
  • 解析:JVM將常量池內的符號引用替換為直接引用的過程。這裡要解釋下符號引用何直接引用。
    • 符號引用: 用一組符號描述所引用的目標,引用的目標不一定已經載入到記憶體中。
    • 直接引用:直接指向目標的指標、相對偏移量、間接定位到目標的控制程式碼,直接引用的目標已經在記憶體中。
  • 初始化,類載入過程的最後一步,執行類構造器<clinit>()方也就是真正執行類中定義的Java程式程式碼。遇到使用new關鍵字例項化物件、讀取或設定一個類的靜態欄位、呼叫一個類的靜態方法這些場景時,如果類沒有被初始化,使用java.lang.reflect包的方法對類進行 反射呼叫時,那麼一定要先初始化。還有一種場景是初始化子類時,如果父類沒有被初始化,那麼要先初始化父類。再看下介面的初始化,與類不同的地方就在於父類的初始化,子介面在使用到父介面的時候才初始化。在多執行緒環境中,如果多個執行緒同時初始化一個類,那麼只有執行緒執行類的<clinit>()方法。

  JVM載入位元組碼檔案靠的是類載入器,這個操作是在JVM外部實現的。這樣應用程式就可以自己決定如何獲取所需的類。如果兩個類來自同一個Class檔案,但是由不同的類載入器載入,那麼者兩個類一定是不相等。從JVM角度講,只有兩種載入器。一種是啟動類載入器,是虛擬機器自身的一部分,由C++語言實現;還有就是其他類載入器,由Java語言實現,全都繼承自抽象類java.lang.ClassLoader 獨立於虛擬機器外部。

  從開發角度看,主要分為這三種:

  • 啟動類載入器(Bootstrap ClassLoader):載入<JAVA_HOME>/jre/lib目錄中,或者被 -Xbootclasspath引數所指定的路徑中。
  • 擴充套件類載入器(Extension ClassLoader):主要載入<JAVA_HOME>/jre/lib目錄中的
  • 應用程式類載入器(Application ClassLoader):載入使用者類路徑(ClassPath)上所指定的類庫。如果我們沒有自定義過自己的類載入器,那麼這就是程式預設的類載入器。

  這些類載入器之間的關係為:

           

  在使用類載入器載入類的過程種,最好遵循雙親委派模型。雙親委派的原理是:類載入器收到載入類的請求時,先把這個請求委派給父類載入去完成,每一層次的載入器按這個這個邏輯執行。那麼所有的載入請求最終都應該傳送到頂層的啟動類載入中。父載入器無法載入,子載入器才會自己載入。這樣做的好處是可以避免類的重複載入,保證程式執行的穩定性。

  我們可以自定義類載入器,總結起來就是:(1)類繼承ClassLoader (2) 重寫findClass() 方法 (3) 呼叫defineClass()方法。在loadClass()裡如果父類載入失敗,呼叫findClass()方法載入,這樣還是符合雙親委派機制的。破壞會雙親委派需要重寫loadClass()方法。

 

  參考資料:《深入理解Java虛擬機器》第二版 周志明

       《深入拆解Java虛擬機器》鄭雨迪

相關文章