Java源文件(.java文件)被編譯器編譯后變為字節碼形式的類文件(.class文件),Java類加載的過程就是JVM加載.class的二進制文件并且放到內存中,將數據放到方法區,并且在堆區構造一個java.lang.class對象,并且完成類的初始化的過程。
//下述片段引用自?Java的類加載機制是什么?
Java的類加載機制主要分為三個過程:加載、連接和初始化。
1.加載機制
Java的類加載機制主要分為三個過程:加載、連接和初始化。這三個過程的順序是固定的,但是每個過程中的細節卻是不同的。下面我們來詳細介紹一下這三個過程。
1.1 加載
Java的類加載器會根據類的全限定名來加載類,當需要使用某個類時,如果該類還未被加載進內存,則需要執行一下步驟進行加載:
1.1.1. 通過類的全限定名找到對應的class文件,這里的class文件可以是.java文件經過編譯之后生成的.class文件,也可以是通過其他方式生成的.class文件。
1.1.2 將class文件中的二進制數據讀取到內存中,并將其轉換為方法區的運行時數據結構。
1.1.3 創建由該類所屬的java.lang.Class對象。該對象可以理解為,是對類的各種數據(如名稱、訪問修飾符、方法、成員變量等)的封裝。
在加載類時,類加載器除了加載某個具體的類外,還需要將這個類所依賴的類也加入到內存中。這種依賴性是多層級的,也就是說,被依賴的類又可能會去依賴其他類,所以在加載一個類時,通常需要將其類圖中所有的類都加載進來。
1.2 連接
Java虛擬機在加載類之后,需要對類進行連接,連接分為三個步驟:驗證、準備和解析。
1.2.1. 驗證:在這個步驟中,Java虛擬機主要確保所加載的類的正確性。驗證過程主要包括文件格式驗證、元數據驗證、字節碼驗證和符號引用驗證等。其目的在于確保目標.class文件的字節流中包含的信息符合當前虛擬機的要求,并且不會危害虛擬機運行時環境安全。
1.2.2. 準備:在準備階段,Java虛擬機為類的靜態變量分配內存,并設置變量的初始值。這里需要注意的是,在這個階段中分配的內存并不包含那些用戶自定義的初始化值,這些值在初始化階段中進行設置。
1.2.3. 解析:Java在這個階段中將常量池中的符號引用轉為直接引用。通過符號引用,虛擬機得知該類訪問其他的類或者類中的字段、方法等,但在類初始化時,需要緩存這些直接引用,以便于直接調用。
1.3 初始化
在類的準備階段,Java虛擬機已經為靜態變量分配了內存并設置了初值,但是這些靜態變量”賦初值“的動作并沒有完成。初始化階段,會為靜態變量設置用戶自定義的初始化值,并執行類構造器<clinit>()方法,以執行初始化操作。
此時,類的準備和初始化階段已經執行結束,Java的類加載機制總的過程也就結束了
//引用結束
注意,靜態代碼的初始化分為兩步,連接的準備階段包含初始化,最后又有一個初始化步驟,兩者并不重疊,前者是給靜態成員變量分配內存并且設置類型的初始值,后者是給靜態成員變量設置用戶指定的初始值。這段話有點拗口,代碼來說明更清晰。
?1、父類 parent.java文件
public class Parent {static {System.out.println("Parent static block 1.");parentStaticIntVar = 3;}static Integer parentStaticIntVar = 2;static {System.out.println("Parent static block 2.");System.out.println("parentStaticIntVar=" + parentStaticIntVar);parentStaticIntVar = 4;}{System.out.println("Parent not static block 1.");parentIntVar = 30;}Integer parentIntVar = 20;public Parent(){ System.out.println("Parent construct method .");System.out.println("parentIntVar=" + parentIntVar);}public void f(){System.out.println("parent f().");}{System.out.println("Parent not static block 2.");parentIntVar = 40;}}
2、子類 Sub.java文件
public class Sub extends Parent {static {System.out.println("Sub static block 1.");subStaticIntVar = 3;}static Integer subStaticIntVar = 2;static {System.out.println("Sub static block 2.");System.out.println("subStaticIntVar=" + subStaticIntVar);subStaticIntVar = 4;}{System.out.println("Sub not static block 1.");subIntVar = 30;}Integer subIntVar = 20;public Sub(){ System.out.println("Sub construct method .");System.out.println("subIntVar=" + subIntVar);}public void f(){System.out.println("Sub f().");}{System.out.println("Sub not static block 2.");subIntVar = 40;}}
3、TestMain.java文件
public class TestMain {public static void main(String[] args) {System.out.println("-----class-----");System.out.println(">>>subStaticIntVar=" + Sub.subStaticIntVar);System.out.println(">>>parentStaticIntVar=" + Sub.parentStaticIntVar);System.out.println("-----instance-----");Sub s = new Sub();s.f();}}
4、輸出結果
-----class-----
Parent static block 1.
Parent static block 2.
parentStaticIntVar=2
Sub static block 1.
Sub static block 2.
subStaticIntVar=2
>>>subStaticIntVar=4
>>>parentStaticIntVar=4
-----instance-----
Parent not static block 1.
Parent not static block 2.
Parent construct method .
parentIntVar=40
Sub not static block 1.
Sub not static block 2.
Sub construct method .
subIntVar=40
Sub f().
解讀一下后可以知道靜態代碼塊和靜態變量遵循如下規則:
1、靜態代碼塊先于構造方法執行;
2、靜態代碼塊可以給靜態成員變量賦值;
3、靜態代碼塊之間按照先后順序執行;
4、父類的靜態代碼塊先于子類的靜態代碼塊執行;
5、靜態代碼塊先于非靜態代碼塊執行;
6、靜態代碼塊在第一次使用這個類的時候執行,并且只執行一次;
7、靜態變量的顯式賦值和靜態代碼塊的按照先后順序執行;
實際上每個Java源文件由編輯器編譯后,會自動給類加載器追加一個類初始化方法:<clinit>(),一個類只有一個,包含靜態變量的顯式賦值代碼和靜態代碼塊的代碼,在源文件中看起來是一個一個獨立的代碼塊,實際上編譯后都放到一個這個類初始化方法中去了。
非靜態的代碼塊和變量的規則和上述類似。
類加載的方法>>>
1、Class.forName("")
Class<?> c = Class.forName("com.example.zhangzk.reflect.TestServiceImpl");
加載類,并且完成初始化。
2、ClassLoader.loadClass("")
Class<?> c = Thread.currentThread().getContextClassLoader().loadClass("com.example.zhangzk.reflect.TestServiceImpl");
加載類,不初始化。
加載器有哪些>>>
啟動類加載器,C++編寫的,進入Java世界的大門,負責加載存放在$JAVA_HOME\jre\lib下,或被-Xbootclasspath參數指定的路徑中的,并且能被虛擬機識別的類庫(如rt.jar,所有的java.*開頭的類均被Bootstrap ClassLoader加載)。
擴展類加載器,Java編寫的,父加載器為啟動類加載器,該加載器由sun.misc.Launcher$ExtClassLoader實現,它負責加載$JAVA_HOME\jre\lib\ext目錄中,或者由java.ext.dirs系統變量指定的路徑中的所有類庫(如javax.*開頭的類)。
應用程序類加載器,Java編寫的,父加載器為擴展類加載器,該類加載器由sun.misc.Launcher$AppClassLoader來實現,它負責加載用戶類路徑(ClassPath)所指定的類,開發者可以直接使用該類加載器,如果應用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認的類加載器。
自定義類加載器,Java編寫的,父加載器為應用程序類加載器,典型代表是Tomcat,為了在一個TOMCAT進程下部署多個JAVA應用程序必須要自定義類加載器,進行應用隔離。
雙親委派模型>>>
每個類加載器需要加載類的時候,先請求父類加載器來加載,一直到啟動類加載器,啟動類加載器是沒有父類加載器的,父類加載器找不到給類才由自己來加載。
要想搞清楚Spring Boot的啟動流程,必須要要知道上述區別,只有充分利用好上述差異才能精準的控制加載和初始化的過程,Spring中這些都用的出神入化了。