類加載機制
虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
類加載的時機
- 遇到new(比如new Student())、getstatic和putstatic(讀取或設置一個類的靜態字段,如下代碼,讀取被final修飾並已在編譯器把結果放入常量池的靜態字段除外)、invokestatic(調用類的靜態方法)這四條指令時,如果對應的類沒有初始化,則要對對應的類先進行初始化。
public class Student{
private static int age;
public static void method(){
}
}
Student.age
Student.method();
- 使用java.lang.reflect包方法時對類進行反射調用的時候。
- 初始化一個類的時候發現其父類還沒初始化,要先初始化其父類。
- 當虛擬機開始啟動時,用戶需要指定一個主類(main),虛擬機會限制性這個主類的初始化。
類加載的過程
類加載過程是如下圖所示的一個流水線過程,其中連接過程可細化為驗證、准備和解析三個小步驟。
加載
class文件–>class對象
“加載”過程主要是靠類加載器實現的,包括用戶自定義類加載器。
加載的過程
在加載過程中,JVM主要做以下3件事:
- 通過一個類的全限定名來獲取定義此類的二進制字節流(class文件)。在程序運行過程中,當要訪問一個類時,若發現這個類尚未被加載,並滿足類初始化的條件時,就根據要被初始化的這個類的全限定名找到該類的二進制字節流,開始加載過程
- 將這個字節流的靜態存儲結構轉化為方法區的運行時數據結構(即Class對象)
- 在內存中創建一個該類的java.lang.Class對象,作為方法區該類的各種數據的訪問入口
程序在運行中所有對該類的訪問都通過這個類對象,也就是這個Class對象是提供給外界訪問該類的接口。
加載源
JVM規范對於加載過程給予了較大的寬松度,一般二進制字節流都從已經編譯好的本地class文件中讀取,此外還可以從這些地方讀取:zip包(jar、war、ear等),由jsp文件中生成對應的Class類,數據庫,網絡,運行時計算生成(動態代理技術)。
加載過程的注意點
- 類和數組加載的區別:非數組類是由類加載器來完成;數組類本身不通過類加載器創建,它是由java虛擬機直接創建,但數組類與類加載器有很密切的關系,因為數組類的元素類型最終要靠類加載器創建。
- HotSpot將Class對象存放在方法區
驗證
各種檢查
驗證階段比較耗時,它非常重要但不一定必要,可用-Xverify:none參數關閉,以縮短類加載時間。
驗證的目的
保證二進制字節流的信息符合虛擬機規范,並沒有安全問題。
驗證的必要性
Java語言的安全性是通過編譯器來保證的,但編譯器和虛擬機是兩個獨立的東西,虛擬機只認二進制字節流,它不會管所獲得的二進制字節流是哪來的。當然,如果是編譯器給它的那么就相對安全,但如果是從其它途徑獲得的,那么無法確保該二進制字節流是安全的。
驗證的過程
其中文件格式驗證階段是基於二進制字節流進行的,只有通過本階段驗證,才被允許存放到方法區。后面的三個驗證階段都是基於方法區的存儲結構進行,不會再直接操作字節流。
准備
為static分配內存並初始化0值。JDK1.7之前在方法區,1.7之后在堆。
僅僅為類變量(即static修飾的字段變量)分配內存並且設置該類變量的初始值即零值,這里不包含用final修飾的static,因為final在編譯的時候就會分配好,同時這里也不會為實例變量分配初始化。類變量(靜態變量)會分配在方法區中,而實例變量是會隨着對象一起分配到Java堆中。
准備階段主要完成兩件事情:
- 為已在方法區中的類的靜態成員變量分配內存;
- 為靜態成員變量設置初始值,具體初始值為下圖所示。
注意:
public static int x = 1000;
實際上變量x在准備階段過后的初始值為0,而不是1000。將x賦值為1000是在初始化階段完成。
解析
將符號引用替換為直接引用
解析是虛擬機將常量池的符號引用替換為直接引用的過程。
初始化
調用
方法
初始化過程就是調用類初始化方法的過程,完成對static修飾的類變量的手動賦值還有主動調用靜態代碼塊。
注意點:此步驟中虛擬機會保證在多線程環境中一個類的
類加載器介紹
啟動類加載器:
由C++實現,不是ClassLoader子類。
負責加載JAVA_HOME\lib目錄中的,或通過-Xbootclasspath參數指定路徑中的,且被虛擬機認可(按文件名識別,如rt,jar)的類。
擴展類加載器:
負責加載JAVA_HOME\lib\ext目錄中的,或通過java.ext.dirs系統變量指定路徑中的類庫。
應用程序類加載器:
負責加載用戶路徑(classpath)上的類庫。
自定義類加載器:
上述的加載器只能加載指定目錄下的jar和class,如果想加載其他位置的jar或類時,則需要實現自定義類加載器來加載。
比如要加載網絡上的一個class文件,通過動態加載到內存之后,要調用這個類中的方法實現特定業務邏輯,此時默認的ClassLoader就不能滿足我們的需求,需要定義自己的ClassLoader。
雙親委派模型
JVM的類加載是通過多層次的類加載器來完成的,類的層次關系和加載順序可以由下圖來描述:
加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader組層檢查,只要某個classloader已加載就視為已加載此類,保證此類只加載一次。而加載的順序是自頂向下,也就是由上層來組層嘗試加載此類。這種類加載的層次關系就是雙親委派模型。
需注意的點:
- 當一個類加載器收到類加載任務,會先交給其父類加載器去完成,因此最終加載任務都會傳遞到頂層的啟動類加載器;
- 只有當父類加載器無法完成加載任務時,才會嘗試執行加載任務。
為什么使用雙親委派這種模型
因為這樣可以避免類的重復加載,當父classloader經加載了該類的時候,就沒必要子classloader再加載一次。
考慮到安全因素,我們試想一下,如果不適用這種委托模型,那我們就可以隨時使用自定義的String來動態替代java核心api重定義的類型,這樣會存在非常大的安全隱患,而雙親委托的方式,就可以避免這種情況。因為String已經在啟動時就被Bootstrap ClassLoader加載,所以用戶自定義的ClassLoader永遠也無法加載一個自己寫的String,除非改編JDK中ClassLoader搜索類的默認算法。
判定兩個Class對象是否相同的依據
- class字節碼是否相同
- ClassLoader是否相同
JVM在判定兩個class是否相同,不僅要判斷兩個類名是否相同,而且要判斷是否由同一個類加載器實例加載的。類的全限定名完全相同,但是加載它的類加載器不同,那么在方法區中會產生不同的Class對象。
只有兩者同時滿⾜的情況下,JVM才認為這兩個class是相同的。就算兩個class是同⼀份class字節碼,如果被兩個不同的ClassLoader實例所加載,JVM也會認為它們是兩個不同class。
破壞雙親委派模型
為什么需要破壞雙親委派
因為在某些情況下父類加載器需要加載的class文件由於受到加載范圍的限制,父類加載器無法加載到需要的文件,這個時候就需要委托子類加載器進行加載。
雙親委派模型是在JDK1.2以后才使用的,但是有一些核心的API類在JDK1.2之前就已經寫好了。
簡單理解雙親委派模型是子類加載器去委托父類加載器完成類加載的工作,而破壞雙親委派模型是父類加載器去委托子類加載器完成類加載的工作。
以Driver接口為例,由於Driver接口定義在jdk當中,而其實現由各個數據庫的服務商來提供,比如mysql就寫了MySQL Connector,這些實現類都是以jar包的形式放到classpath目錄下。
那么問題就來了,DriverManager(也由jdk提供,JDK1.2之前就寫好了)要加載各個實現了Driver接口的實現類(在classpath下),然后進行管理,但是DriverManager由啟動類加載器加載,只能加載JAVA_HOME\lib下的文件,而其實現是由服務商提供的,有系統類加載器加載,這個時候就需要啟動類加載器來委托子類加載器來加載Driver實現,從而破壞了雙親委派。如下圖所示。
最后
歡迎關注公眾號:前程有光,領取一線大廠Java面試題總結+各知識點學習思維導+一份300頁pdf文檔的Java核心知識點總結!