
一、類加載時機
1.1 觸發類初始化的六個場景: 加載?
1、遇到new、getstatic、putstatic或invokestatic這四條位元組碼指令時
如果型別沒有進行過初始化,則需要先觸發其初始化階段,生成這四條指令的Java代碼場景是:
- 使用new關鍵字實體化物件的時候
- 讀取或設定一個型別的靜態欄位(被final修飾、已在編譯期把結果放入常量池的靜態欄位除外)的時候
- 呼叫一個型別的靜態方法的時候
2、 使用java.lang.reflect包的方法對型別進行反射呼叫的時候,如果型別沒有進行過初始化,則需要先觸發其初始化,
3、當初始化類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化,
4、當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類,
5、 當使用JDK 7新加入的動態語言支持時,如果一個java.lang.invoke.MethodHandle實體最后的決議結果為REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種型別的方法句柄,并且這個方法句柄對應的類沒有進行過初始化,則需要先觸發其初始化,
6、當一個介面中定義了JDK 8新加入的默認方法(被default關鍵字修飾的介面方法)時,如果有這個介面的實作類發生了初始化,那該介面要在其之前被初始化,
注意:以上情況稱為稱對一個類進行“主動參考”,除此種情況之外,均不會觸發類的初始化,稱為“被動參考”
1.2 被動參考的例子
public class SuperClass {
static {
System.out.println("SuperClass init!");
}
public static int value = https://www.cnblogs.com/JianJianHuang/p/123;
}
public class SubClass extends SuperClass{
static {
System.out.println("SubClass init!");
}
}
-
通過子類參考父類的靜態欄位,不會導致子類的初始化
/** * 被動參考: 演示一 * 非主動使用類欄位演示 * jvm args: -XX:+TraceClassLoading 列印類加載軌跡 * @author hdj */ public class NoInitializationWithStaticField { public static void main(String[] args) { System.out.println(SubClass.value); } } //輸出 SuperClass init! 123所以,對于靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過其子類來參考父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化,
-
通過陣列定義來參考類,不會觸發此類的初始化
public class NoInitializationWithArrayRef { public static void main(String[] args) { SuperClass[] sca = new SuperClass[10]; } } //輸出, 可以發現沒有輸出SuperClass init!注意: 這段代碼里面觸發了另一個名為“[Lcn.hdj.jvm.classloading.SuperClass”的類的初始化階段,它是一個由虛擬機自動生成的、直接繼承于java.lang.Object的子類,創建動作由位元組碼指令newarray觸發,這個類代表了一個元素型別為cn.hdj.jvm.classloading.SuperClass的一維陣列
-
常量在編譯階段存入呼叫類的常量池中,本質上沒有直接參考到定義常量的類,因此不會觸發定義的常量
public class ConstClass { static { System.out.println("ConstClass init!"); } public static String HELLOWROLD="helloworld"; } /** * 被動參考: 演示三 * 常量在編譯階段存入呼叫類的常量池中,本質上沒有直接參考到定義常量的類,因此不會觸發定義的常量 * * @author hdj */ public class NoInitializationWithConst { public static void main(String[] args) { System.out.println(ConstClass.HELLOWROLD); } } //輸出 helloworld注意:這里之所以沒有輸出"ConstClass init!", 是因為在編譯階段通過常量傳播優化,已經將此常量的值“hello world”直接存盤在NoInitializationWithConst類的常量池中,以后NoInitializationWithConst對常量ConstClass.HELLOWORLD的參考,實際都被轉化為NoInitializationWithConst類對自身常量池的參考了,
1.3 介面加載程序
介面也有初始化程序,這點也類是一致的,介面中不能使用static{} 靜態代碼塊,但編譯器仍然會為介面生成<clinit>()構造器,用于初始化介面中所定義的成員變數,介面初始化與類不同的是: 當一個類初始化時,要求其父類全部初始化完成,但一個介面初始化時,并不要求其父介面全部初始化完成,只有真正使用到父介面時才會初始化,
二、類加載程序
2.1 加載(Loading): 找Class檔案
2.1.1 Java虛擬機在加載階段要完成三件事:
- 通過一個類的全限定名來獲取定義該類的二進制子節流
- 將子節流所代表的靜態存盤結構轉化為方法區的運行時資料結構
- 在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口,
2.1.2 陣列加載的遵循規則
- 如果陣列的組件型別(Component Type)是參考型別,則遞回通過類加載器加載(一個類必須與類加載器確定唯一性)
- 如果陣列的組件型別不是參考型別,虛擬機會將陣列標記為與引導類加載器關聯
- 陣列的可見性與它的組件型別可見性一致,如果組件型別不是參考型別,那陣列的的可見性將默認為public
2.2 驗證(Verification):驗證格式、依賴
驗證是連接階段的第一步,這個階段的目的是為了確保Class檔案的子節流中包含的資訊符合當前虛擬機的要求,并且不會危害虛擬機的自身安全,分為以下四個階段的校驗,
-
檔案格式驗證
-
元資料校驗
-
位元組碼校驗
-
符號參考校驗
對應虛擬機來說,驗證階段是一個非常重要的,但不是一定必要(對于程式運行期沒有影響)的階段,所以可以部署時通過虛擬機引數
-Xverify:none來關閉驗證,縮短虛擬機加載時間,
2.3 準備(Preparation):靜態欄位、方法表
? 準備階段是正式為類變數分配記憶體并設定類變數初始值的階段,這些變數所使用的記憶體都將在方法區中進行分配,
注意: 這個階段進行分配的變數僅僅是類變數(被static修飾的變數),而實體變數將會在物件實體化時隨著物件一起分配在Java堆中,還有這個階段的初始化,"通常情況"下是資料型別的零值,
public static int value = https://www.cnblogs.com/JianJianHuang/p/123;
上述變數value 在準備階段過后的初始化值為0而不是123,因為這時候尚未開始執行任何Java方法,而把value 賦值123的putstatic 指令是在程式被編譯后,存放在類構造器<clinit>()方法之中,所以把value 賦值123的動作在初始化階段才會執行,
- 基本資料型別的零值
| 資料型別 | 零值 | 資料型別 | 零值 |
|---|---|---|---|
| int | 0 | boolean | false |
| long | 0L | float | 0.0f |
| short | (short)0 | double | 0.0d |
| char | '\u0000' | reference(參考型別) | null |
| byte | (byte)0 |
- 特殊情況下會初始化指定值
如果類欄位的欄位屬性存在ConstantValue屬性,則會在準備階段變數value會初始化為ConstantValue屬性所指定的值,如以下定義的欄位
public static final int value = https://www.cnblogs.com/JianJianHuang/p/123;
2.4 決議(Resolution):符號決議參考
? 決議階段是虛擬機將常量內的符號參考替換為直接參考的程序,
-
符號參考(Symbolic References)
- 符號參考以一組符號來描述所參考的目標,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可,
- 符號參考與虛擬機實作的布局無關,參考的目標不一定已經加載到記憶體中,
- 各種虛擬機實作的記憶體布局可以各不相同,但是它們能接受的符號參考必須都是一致的,應為符號參考的字面量形式明確定義在Java虛擬機規范的Class檔案格式中,
-
直接參考(Direct References)
- 直接參考可以是直接指向目標的指標、相對偏移量或者是一個能間接定位到目標的句柄,
- 直接參考是和虛擬機實作的記憶體布局相關的,同一個符號參考在不同虛擬機實體上翻譯出來的直接參考一般不會相同,
- 如果又有了直接參考,那參考的目標必定在記憶體中存在,
-
決議階段的具體時間
虛擬機規范中并沒有規定決議階段發生的具體時間,只要求在執行以下16個用于運算子號參考的位元組碼指令前,先對它們所使用的符號參考進行決議,
anewarray checkcast getfeild getstatic instanceof invokedynamic invokedinterface invokedspecial invokestatic invokevirtual ldc ldc_w multianewarray new putfield putstatic -
類或介面的決議
假設在當前代碼所在的類為D,如果要把一個從未決議過的符號參考N決議為一個類或者介面C的直接參考,虛擬機完成決議程序需要以下3個步驟:
- 1、如果C不是一個陣列型別,那虛擬機將會把代表N的全限定名傳遞個D的類加載器去加載這個類C,在加載程序中,由于元資料驗證、位元組碼驗證的需要,又可能觸發其他相關類的加載動作,例如加載這個類的父類或實作的介面,一旦這個加載程序出現任何例外,決議程序就宣告失敗,
- 2、如果C是一個陣列型別,并且陣列元素型別為物件,也就是N的描述符會是類似 "[Ljava/lang/Integer" 的形式,那將會按照第一點的規則加載陣列元素型別,如果N的描述符如前面所假設的形式,需要加載的元素型別就是 "java.lang.Integer",接著有虛擬機生成一個代表此陣列維度和元素的陣列物件,
- 3、如果上面步驟正常執行,那么C在虛擬機中實際上已經成為一個有效的類或者介面了,但決議完成之前還要進行符號參考驗證,確認D是否具備對C的訪問權限,如果發現不具備訪問權限,將會拋出
java.lang.IllegalAccessError例外
-
欄位決議
如果在決議這個類或介面符號參考的程序中出現了任何例外,都會導致欄位符號參考決議的失敗,假設這個欄位所屬的類或介面用C表示,欄位決議分為以下步驟
- 1、如果C本身就包含簡單名稱和欄位描述都與目標相匹配的欄位,則回傳這個欄位的直接參考,查找結束,
- 2、否則,如果在C中實作介面,將會按照繼承關系從下往上遞回搜索各個介面和它父介面,如果介面中包含簡單名稱和欄位描述都與目標相匹配的欄位,則回傳這個欄位的直接參考,查找結束,
- 否則,如果C不是java.lang.Object的話,將會按照繼承關系從下往上遞回搜索其父類,如果父類中包含簡單名稱和欄位描述都與目標相匹配的欄位,則回傳這個欄位的直接參考,查找結束,
- 否則,查找失敗,拋出java.lang.NoSouchFieldError例外
- 正常回傳參考后,將會對這個欄位進行權限驗證,如果發現不具備該欄位的訪問權限,將會拋出
java.lang.IllegalAccessError例外
package cn.hdj.jvm.classloading; /** * @Description: 欄位決議, 欄位參考不明確導致決議失敗 * @Author huangjiajian * @Date 2021/4/10 下午11:01 */ public class FieldResolution { interface Interface0 { int A = 0; } interface Interface1 extends Interface0 { int A = 1; } interface Interface2 { int A = 0; } static class Parent implements Interface1 { public static int A = 3; } static class Sub extends Parent implements Interface2 { //注釋欄位A //這時因為父類Parent 和介面 Interface2都包含欄位A,導致決議欄位參考不明確,決議失敗 //public static int A = 4; } public static void main(String[] args) { System.out.println(Sub.A); } } -
類方法決議
類方法決議與欄位決議步驟一樣,也是需要先決議類方法表的class_index中索引的方法所屬的類或介面符號參考,決議成功才進行類方法決議,假設這個類方法所屬的類或介面用C表示,決議分為以下步驟:
- 1、類方法和介面方法符號參考的常量型別定義是分開的,如果在類方法表中發現class_index中索引的C是個介面,那就直接拋出java.lang.IncompoatibleClassChangeError例外.
- 2、如果C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束,
- 3、否則,如果在C的父類中遞回查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束,
- 4、否則,在類C實作的介面串列或父介面中遞回查找是否有簡單名稱和描述符都與目標想匹配的方法,如果存在匹配的方法,說明類C是一個抽象類,這時查找結束,拋出java.lang.AbstactMethodError例外,
- 5、否則,查找失敗,拋出java.lang.NoSouchFieldError例外
- 6、正常回傳參考后,將會對這個方法進行權限驗證,如果發現不具備該方法的訪問權限,將會拋出
java.lang.IllegalAccessError例外
-
介面方法決議
假設這個類方法所屬的類或介面用C表示,決議分為以下步驟:
- 1、與類方法決議不同,如果在介面方法表中發現class_index索引C是個類而不是介面,那就拋出java.lang.IncompoatibleClassChangeError例外.
- 2、否則,在介面C中查找是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束,
- 3、否則,在介面C的父介面中遞回查找,直到java.lang.Object類為止,看是否有簡單名稱和描述符都與目標相匹配的方法,如果有則回傳這個方法的直接參考,查找結束,
- 4、否則,查找失敗,拋出java.lang.NoSouchFieldError例外
- 5、由于介面中的所有方法都是public的,所以不存在訪問權限問題,
2.5 初始化(Initalization):構造器、靜態變數賦值、靜態代碼塊
? 類初始化是類加載程序的最后一步,前面的類加載程序中,除了在加載階段用戶應用程式可以通過自定義類加載器參與外,其余動作完全有虛擬機主導和控制,
? 初始化階段是執行類構造器<clinit>()方法的程序,
-
<clinit>()方法是有編譯器自動收集類中的所有類變數的賦值動作和靜態陳述句塊(static{})中陳述句合并產生的,編譯器收集的順序是有陳述句在源檔案中出現的順序所決定的,靜態陳述句塊中只能訪問到定義在靜態陳述句塊之前的變數,定義在它之后的變數,在前面的靜態陳述句塊可以賦值,但是不能訪問public class InitOrder { static { i = 0; //可以賦值 //System.out.println(i); //編譯器,提示 Illegal forward reference (非法向前參考) } //定義在靜態代碼塊后的靜態變數 static int i = 1; } -
<clinit>()方法與類的構造器函式(<init>())不同,他不需要顯式地呼叫父類構造器,虛擬機會保證在子類的<clinit>()方法執行之前,父類的<clinit>()方法執行已經完畢,因此在虛擬機中第一個被執行的<clinit>()方法的類肯定是java.lang.Object, -
由于父類的
<clinit>()方法先執行,也意味著父類中定義的靜態陳述句塊要優先與子類的變數賦值操作
-
<clinit>()對于類或介面來說不是必需的,如果一個類中沒有靜態陳述句塊,也沒有對靜態變數的賦值操作,那么編譯器可以不為這個類生成<clinit>()方法, -
介面中不能使用靜態陳述句塊,但仍然有靜態變數初始化的賦值操作,因此介面與類一樣都會生成
<clinit>()方法,但介面與類不同的是,執行介面的<clinit>()方法不需要先執行父介面的<clinit>()方法,只有當父介面中定義的變數使用時,父介面才會初始化,另外,介面的實作類在初始化時也一樣不會執行介面的<clinit>()方法, -
虛擬機會保證一個類
<clinit>()方法在多執行緒環境中被正確地加鎖、同步,如果<clinit>()方法執行很耗時,那么會造成多個執行緒阻塞,public class DeadLoopClassTest { static class DeadLoopClass{ static { if (true) { System.out.println(Thread.currentThread() + "init DeadLoopClass"); while (true) { } } } } public static void main(String[] args) { Runnable runnable=new Runnable() { @Override public void run() { System.out.println(Thread.currentThread() + "start"); DeadLoopClass deadLoopClass=new DeadLoopClass(); System.out.println(Thread.currentThread() + "run Over"); } }; Thread thread1=new Thread(runnable); Thread thread2=new Thread(runnable); thread1.start(); thread2.start(); } } //輸出 Thread[Thread-0,5,main]start Thread[Thread-1,5,main]start Thread[Thread-1,5,main]init DeadLoopClass -
注意:
<clinit>()在同一類加載器下,一個型別只會初始化一次,
三、類加載器
3.1 類與類加載器
-
加載類的類加載器不同,則兩個類一定相等
-
"相等",包括代表類的Class 物件的equals()方法、isAssignableFrom()方法、isIntance()方法、instanceof 關鍵字判定
-
以下使用兩個不同類加載器加載相同的類,使用instanceof判定是否為同一個類
public class ClassLoaderTest2 { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException { ClassLoader classLoader = new ClassLoader() { @Override public Class<?> loadClass(String name) throws ClassNotFoundException { try { String fileName = name.substring(name.lastIndexOf('.') + 1) + ".class"; InputStream inputStream = this.getClass().getResourceAsStream(fileName); if (inputStream == null) { return super.loadClass(name); } byte[] buffer = new byte[inputStream.available()]; inputStream.read(buffer, 0, buffer.length); return this.defineClass(name, buffer, 0, buffer.length); } catch (Exception e) { throw new ClassNotFoundException(name); } } }; Object o = classLoader.loadClass("cn.hdj.jvm.classloading.ClassLoaderTest2").newInstance(); System.out.println(o.getClass()); System.out.println(o instanceof cn.hdj.jvm.classloading.ClassLoaderTest2); } } //輸出 class cn.hdj.jvm.classloading.ClassLoaderTest2 false
3.2 雙親委派模型

-
啟動類加載器(Bootstrap ClassLoader): 這個類加載器負責加載以下路徑的類別庫加載到虛擬機記憶體中
<JAVA_HOME>\lib路徑-Xbootclasspath指定的路徑- 指定路徑下的類別庫,還要能被虛擬能識別的類別庫,如僅按照檔案命名識別類別庫
rt.jar - 注意,啟動類加載器無法被Java程式直接參考
-
擴展類加載器( Extension ClassLoader):這個加載器由
sun.misc.Launcher$ExtClassLoader實作,負責加載以下規則定義的類別庫<JAVA_HOME>\lib\ext路徑下的類別庫java.ext.dirs系統變數所指定的路徑下的類別庫- 開發者可以直接使用擴展類加載器
-
應用程式類加載器(Application ClassLoader,也稱為系統類加載器):這個類加載器由
sun.misc.Launcher$AppClassLoader實作,負責加載用戶類路徑( ClassPath )上指定的類別庫- 一般情況下,是程式中默認的類加載器
- 開發者可以直接使用擴展類加載器
-
雙親委派模型作業程序:
- 當前類加載器收到類加載的請求,不會自己去嘗試加載這個類,先請求委派父類加載器去完成,
- 因此所有加載請求最終都應該傳送到頂層的啟動類加載器中
- 只有當類加載器反饋自己無法完成這個加載請求,子加載器才會嘗試自己加載
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/274994.html
標籤:其他
