類加載子系統
類加載的時機
類加載的時機主要有 4 個:
- 遇到
new、getstatic、putstatic、invokestatic
這四條字節碼指令時,如果對應的類沒有初始化,則要先進行初始化- new 關鍵字創建對象時
- 讀取或設置一個類型的靜態字段時(被 final 修飾、已在編譯器將結果放入常量池的靜態類型字段除外)
- 調用一個類型的靜態方法的時候
- 對類進行
反射調用
時 - 初始化一個類的時候,如果其父類未初始化,要先初始化其父類
- 虛擬機啟動時,要先加載主類(程序入口)
類加載過程
類的生命周期如下圖:
-
加載
- 通過二進制字節流加載 class 文件
- 創建該 class 文件在方法區的運行時數據結構
- 創建字節碼對象 Class 對象
-
鏈接
-
驗證:目的在于確保 class 文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性
主要包括四種驗證:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證
-
準備:為類變量(即靜態變量)分配內存并且設置類變量的默認初始值,即零值。
這里不包含用 final 修飾的 static 變量,因為 final 修飾的變量在編譯為 class 字節碼文件的時候就會分配了,準備階段會顯式初始化
這里不會為實例變量分配初始化,類變量會分配在方法區,而實例變量是會隨著對象一起分配到 Java 堆中
-
解析:將常量池內的符號引用轉換為直接引用的過程
事實上,解析操作往往會伴隨著 JVM 在執行完初始化之后再執行
符號引用就是一組符號來描述所引用的莫表。符號引用的字面量形式明確定義在《java虛擬機規范》的Class 文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。
-
-
初始化
虛擬機在初始化階段才真正開始執行類中編寫的 Java 程序代碼
初始化階段就是執行類構造器
<clinit>()
方法的過程,<clinit>()
是 Javac 編譯器自動生成的,該方法由編譯器自動收集類中的所有類變量的賦值動作和靜態語句塊中的語句合并生成的,如果一個類中沒有靜態代碼塊, 也沒有變量賦值的動作,那么編譯器可以不為這個類生成<clinit>()
方法
類加載器
JVM 中類加載是通過類加載器來完成的
- 啟動類加載器(Bootstrap ClassLoader):
- 負責加載
JAVA_HOME\lib
目錄中的,或通過-Xbootclasspath
參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt.jar)的類。由 C++ 實現,不是 ClassLoade r的子類
- 負責加載
- 擴展類加載器(Extension ClassLoader):
- 負責加載
JAVA_HOME\lib\ext
目錄中的,或通過java.ext.dirs
系統變量指定路徑中的類庫。
- 負責加載
- 應用程序類加載器(Application ClassLoader):
- 負責加載用戶路徑
classpath
上的類庫
- 負責加載用戶路徑
- 自定義類加載器(User ClassLoader):
- 作用:JVM自帶的三個加載器只能加載指定路徑下的類字節碼,如果某些情況下,我們需要加載應用程序之外的類文件,就需要用到自定義類加載器
雙親委派機制
加載類的class文件時,Java虛擬機采用的是雙親委派機制
,即把請求交給父類加載器去加載
工作原理:
- 如果一個類加載器收到了類加載請求,他并不會自己先去加載,而是把這個請求委托給父類的加載器去執行
- 如果父類加載器也存在其父類加載器,則繼續向上委托
- 如果父類加載器可以完成類加載任務,就成功返回;如果父類加載器無法完成類加載任務,則會由自家在其嘗試自己去加載
優勢:
- 避免類的重復加載
- 保護程序安全,防止核心API被篡改(例如,如果我們自定義一個java.lang.String類,然后我們去new String(),我們會發現創建的是jdk自帶的String類,而不是我們自己創建的String類)
為什么還需要破壞雙親委派?
- 在實際應用中,可能存在 JDK 的基礎類需要調用用戶代碼,例如:SPI 就打破雙親委派模式(打破雙親委派意味著上級委托下級加載器去加載類)
- 比如,數據庫的驅動,Driver 接口定義在 JDK 中,但是其實現由各個數據庫的服務上提供,由系統類加載器進行加載,此時就需要
啟動類加載器
委托子類加載器去加載 Driver 接口的實現
- 比如,數據庫的驅動,Driver 接口定義在 JDK 中,但是其實現由各個數據庫的服務上提供,由系統類加載器進行加載,此時就需要