一 類的加載階段
類加載具體做的是什麼
# 根據類的權限定名,獲取此類的二進制流(文件或者網絡等)
# 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據
# 在內存創建一個代表這個Class的對象,然後作爲數據的訪問入口
二 類的連接階段
2.1 驗證階段
驗證的目的是確保加載的Class文件的字節碼流的信息符合Java虛擬機規範,不會危害虛擬機的安全。
包括文件格式驗證
元數據驗證
字節碼驗證
符號引號驗證
2.2 準備階段
爲類變量分配內存並且設置變量的初始值。
2.3 解析階段
將虛擬機中常量池中符號引用替換爲直接飲用的過程。
類或者接口的解析
字段解析
類方法解析
接口方法解析
三 類的初始化
初始化時類加載的最後一步,執行<clinit>方法的過程,clinit實例化對象的時候執行的構造方法。說白了就是類的執行構造方法。
3.1 init和clinit比較
編譯器編譯完字節碼之後,字節碼文件常量池會存放<init>和<clinit>方法名
init: 實例構造方法,會將實例代碼塊,變量初始化,調用父類構造器等操作收集,順序爲:
父類變量初始化 -> 父類語句塊 -> 父類構造函數 -> 子類變量初始化 -> 子類語句塊 -> 子類構造函數
clinit:類構造方法,會將靜態代碼塊,靜態變量初始化後,收集起來,他們沒有固定的順序,編譯器收集的順序就是源代碼中他們出現的順序。
所以有可能出現靜態構造方法在前,變量初始化在後,但是在靜態代碼塊卻無法使用的問題
比如:
public class Base {
static {
num = 20;
System.out.println(num); // 編譯報錯
}
public static int num = 10;
public static final String CONST = "const";
}
問題一: 爲什麼 num = 20 不報錯呢?
因爲在類加載過程中的鏈接階段的準備階段,就會爲類變量分配內存並且設置變量的初始值。所以num = 20不會報錯
問題二:但是靜態代碼塊訪問num就編譯報錯呢?
因爲編譯器收集順序是根據靜態變量(類變量)在文件中順序決定的,靜態代碼塊可以訪問靜態代碼塊之前的變量,但是不能訪問之後的變量。
如果這樣改就好了:
public class Base {
public static int num = 10;
static {
num = 20;
System.out.println(num); // 編譯報錯
}
public static final String CONST = "const";
}
clinit方法是在類加載過程(初始化)中執行的,而init是在對象實例化執行的,所以clinit一定比init先執行。
public class Parent {
public static int a = 1;
int b = 1;
public Parent() {
System.out.println("Parent Init Method......");
}
{
System.out.println("Parent Instance Code Block => "+ (++b));
}
static {
System.out.println("Parent Static Code Block => "+ (++a));
}
}
public class Child extends Parent {
public static int a = 1;
int b = 1;
public Child() {
System.out.println("Child Initial Method......");
}
{
System.out.println("Child Instance Code Block => "+ (++b));
}
static {
System.out.println("Child Static Code Block => "+ (++a));
}
public static void main(String[] args) {
new Child();
}
}
結果:
Parent Static Code Block => 2
Child Static Code Block => 2
Parent Instance Code Block => 2
Parent Init Method......
Child Instance Code Block => 2
Child Initial Method......
3.2 接口
接口中不能有靜態代碼塊,但是接口可以賦值,因此接口和類一樣也會生成<clinit>方法,但是接口的clinit方法不需要先執行父類的clinit方法,只有當父接口中的變量使用時,父接口才會被初始化。
3.3什麼是符號引用和直接引用,爲什麼需要在常量池定位到符號的引號?
符號引用:
就是用一組符號來描述所引用的目標,符號可以是任何形式的,只要使用時能夠定位到目標即可。
我們知道Java類編譯成class文件,編譯的時候,該類並不知道其所引用類的實際內存地址,因此只能使用符號引用來代替。
直接引用:
直接飲用就是直接指向目標的指針,比如指向對象,或者類方法,類變量的指針,亦或者指向實例變量和實例方法的指針
3.4 類的初始化時機
# 遇到new getstatic putstatic invokestatic指令的時候,如果類沒有進行過初始化,則需要先進行初始化。場景如下:使用new關鍵字實例化對象,讀取或者設置一個類的靜態字段(被final修飾,已經被編譯器把結果放入常量池的靜態字段除外,比如:private static final String A = “A”),或者調用一個類的靜態方法的時候。
# 使用Java反射的時候,如果沒有對類進行過初始化,則需要先初始化
# 當初始化一個類的時候,如果父類還沒有初始化過,則先初始化其父類
# 虛擬機啓動的時候,用戶需要指定一個執行的主類,即main方法所在類,虛擬機會先初始化這個類。
3.5 不會被初始化場景
3.5.1通過子類訪問父類的靜態變量,並不會加載子類,只會加載父類
public class
Base {
static {
System.out.println("基類被初始化了......");
}
public staticint num
= 10;
}
public class Apex extends Base{
static {
System.out.println("Apex被初始化了......");
}
}
public class LoadTimes {
public static void main(String[] args) {
System.out.println(Apex.num);
}
}
結果:並沒有初始化子類
基類被初始化了......
10
3.5.2 創建一個對象的數組對象,父類和子類都不會被加載
public class LoadTimes {
public static void main(String[] args) {
Apex[] apexes = new Apex[5];
}
}
3.5.3 訪問一個類的常量,該類不會被加載
public class Base {
static {
System.out.println("基類被初始化了......");
}
public static int num = 10;
public static final String CONST = "const";
}
public class LoadTimes {
public static void main(String[] args) {
System.out.println(Base.CONST);
}
}
結果,只是打印了值const,並沒有加載這個類
四 類加載器
通過一個類的全限定名來獲取此類的二進制字節碼流的這個動作放到Java虛擬機外部去實現,以便讓應用程序決定如何獲取所需要的類。
只有被同一個類加載器加載加載的類纔有可能會相等,相同的字節碼被不同的類加載器加載的類不相等。
4.1 類加載分類
4.1.1 啓動類加載器
C++實現,是虛擬機的一部分,用於加載JAVA主目錄下lib目錄下的類
4.1.2 擴展類加載器
用於加載JAVA主目錄下lib/ext目錄下的類
4.1.3 應用程序類加載器
加載用戶classpath(類路徑)上的所指定的類庫
4.1.4 自定義類加載器
定義一個類繼承ClassLoader,重寫loadClass方法,實例化Class對象
JVM 已經有了以上的類加載器,爲什麼還要使用自定義類加載器呢?
因爲有的時候可能需要熱部署或者代碼加密。
4.2 雙親委派模型 (類加載器如何協同工作)
雙親委派模型:即ParentDelegation Model. 要求除了啓動類加載器之外,其他的加載器都應該有自己的父類加載器。他們的父子關係並不是繼承來實現,而是都使用組合關係來複用父類加載器的代碼。
工作流程:
當有類加載的請求到來的時候,類加載器首先不會自己去加載這個類,而是把這個請求交給它的父類加載去完成,每一個層次的類加載器都是如此,因此所有的類加載請求最終都會達到啓動類加載器。只有當父類加載器表示自己無法完成這個加載請求(在搜索範圍內沒有找到所需要的類,比如啓動類加載器沒有在JAVA_HOME/lib目錄下找到這個類或者擴展加載器沒有在JAVA_HOME/lib/ext目錄搜索到指定的類),子類加載器纔會去嘗試自己去加載。