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


類加載子系統

類文件首先需要經過類加載子系統,進行加載,進類信息等加載到運行時數據區,生成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年我寫了什么?

開源刷題筆記

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM