JVM筆記 -- 來,教你類載入子系統

第十六封發表於2021-03-17

類載入子系統

類檔案首先需要經過類載入子系統,進行載入,進類資訊等載入到執行時資料區,生成Klass的例項。

在類載入子系統中有以下3個階段操作(廣義上的載入):

  • 載入階段
    • Bootstrap ClassLoader:引導類載入器,主要載入JDK裡面的核心類
    • Extension ClassLoader:擴充類載入器
    • Application ClassLoader:應用載入器
  • 連結階段
    • 驗證
    • 連結
    • 解析
  • 初始化階段


如果載入的時候失敗了,則不會執行後面的連結等操作。

類載入子系統的作用:

  • 類載入器子系統可以從本地檔案或者網路中載入Class檔案,Class檔案開頭有特定標識“CAFEBABY”(魔數)。
  • 類載入器只負責將檔案載入到執行時資料區,但是否可以執行,是執行引擎管的
  • 載入的類資訊存放在方法區中,除了類資訊以外,方法區還存放了執行時產量池資訊,可能HIA包括字串字面量和數字常量(這部分常量是Class檔案中常量池部分的記憶體對映)。

譬如反編譯後,會產生常量資訊,裡面包括常量以及符號引用等:

類載入器ClassLoader的角色,以下面的People.class為例:

通過類資訊例項,可以通過new 例項化物件,也可以通過getClassLoader()獲取類載入器,也可以通過例項getClass()獲取類資訊例項。

  1. People.class 存在本地硬碟上,相當於一個模板,最終可以例項化出n個同一個類但是屬性不同的例項。
  2. People.class載入到JVM中,被稱為DNA後設資料模板,存放在方法區,也就是類資訊。類資訊也是物件。
  3. 從.class檔案,到載入到JVM中,稱為後設資料模板,這個過程需要一個轉換工具,這個工具就是類載入器(Class Loader)。

載入(Loading)

此處的載入,指的是類載入過程中的第一個階段(環節),主要工作包括:

  • 1.通過類的全限定名獲取定義此類的二進位制位元組流。
  • 2.將這個二進位制位元組流所代表的靜態儲存結構轉化為方法區(JDK7以及之前叫永久代,JDK8之後成為元空間)的執行時資料結構。
  • 3.在記憶體中生成一個該類的java.lang.Class物件,作為方法區該類的各種資料的訪問入口,也就是類資訊物件。

類的.class檔案來源方式包括以下:

  • 本地系統直接載入
  • 網路傳輸獲取
  • 從zip壓縮包讀取
  • 執行的時候計算生成,譬如動態代理技術
  • 由其他檔案生成,譬如場景:JSP
  • 從加密檔案中解密獲得

連結

連結階段又分為3個階段:

  • 驗證:
    • 目的是校驗安全和法,確保Class檔案的位元組流中包含資訊符合當前虛擬機器要求,保證載入的類的正確性,不會危害到虛擬機器的安全。
    • 主要包括4種驗證:
      • 檔案格式驗證(譬如檔案開頭是"CAFEBABY")
      • 後設資料驗證
      • 位元組碼驗證
      • 符號引用驗證
  • 準備:
    • 為類變數(static)分配記憶體並且設定該變數的預設初始值,即零值
    • 不包含final修飾的static,因為final在編譯的時候已經分配了,準備階段會顯示初始化。
    • 不會為例項變數分配初始化,類變數會分配在方法區,但是例項變數是跟隨物件一起分配在Java堆裡面(一般情況)
  • 解析:
    • 將常量池的符號引用轉化成為直接引用的過程
    • 事實上,解析操作往往會伴隨JVM在執行完初始化之後再執行
    • 符號引用就是一組符號來描述所引用的目標,《Java虛擬機器規範》的Class檔案格式中,直接引用就是直接指向目標的指標,相對偏移量或者一個間接定位到目標的控制程式碼。
    • 解析這個階段,主要是針對類或者介面,欄位,類方法,介面方法,方法型別等等,對應的常量池中的CONSTANT_Class_info,CONSTANT_Fieldred_info,CONSTANT_Methodref_info等。

初始化

初始化,就是執行類的構造器<clinit>()的過程,注意<clinit>()是類的構造器,不是物件的。<clinit>()是初始化類的,就是把類裝到JVM裡的初始化,不是執行時物件的初始化。

<clinit>()這個方法不需要顯式定義,而是javac編譯器自動收集類中的所有變數的賦值動作,加上靜態程式碼塊,合併成的一個方法。

<clinit>()中程式碼的順序和我們在類檔案寫的順序一致。

執行子類的<clinit>()方法之前,JVM會保證先執行其父類的<clinit>(),預設父類是Object

仔細觀察上面的程式碼,會發現,final的屬性,即使是static修飾的,在<clinit>()裡面都不會存在,這是為什麼呢?

這是因為final修飾的是常量,常量不會在初始化的時候執行賦值!!!常量在編譯的時候已經分配了,準備階段會顯示初始化。

如果我們將final去掉,就可以發現,去掉final修飾,位元組碼就會加上該欄位的賦值:(下面的ldc是指常量池的意思,從常量池編號為#6的地方,載入該常量)

虛擬機器在初始化的時候,已經保證了類的<clinit>()方法,即使在多執行緒的環境下,也只會執行一次,其底層的邏輯就是預設同步加鎖了。

【作者簡介】
秦懷,公眾號【秦懷雜貨店】作者,技術之路不在一時,山高水長,縱使緩慢,馳而不息。個人寫作方向:Java原始碼解析,JDBC,Mybatis,Spring,redis,分散式,劍指Offer,LeetCode等,認真寫好每一篇文章,不喜歡標題黨,不喜歡花裡胡哨,大多寫系列文章,不能保證我寫的都完全正確,但是我保證所寫的均經過實踐或者查詢資料。遺漏或者錯誤之處,還望指正。

2020年我寫了什麼?

開源刷題筆記

平日時間寶貴,只能使用晚上以及週末時間學習寫作,關注我,我們一起成長吧~

相關文章