1、類加載時機:java
一、類初始化時機程序員
1)遇到new、getstatic、putstatic或invokestatic這四個字節碼指令時,若是類沒有進行過初始化,則須要先對其進行初始化。數據庫
2)使用Java.lang.reflect包的方法對類進行反射調用的時候,若是類沒有進行過初始化,則須要先出發器初始化。數組
3)當初始化一個類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化安全
4)當虛擬機啓動時,用戶須要制定一個執行的主類,虛擬機會先初始化這個主類網絡
5)當使用jdk1.7動態語言支持時,若是一個Java.lang.invoke.MethodHandle實例最後的解析結果REF_getstatic,REF_putstatic,REF_invokestatic的方法句柄,而且這個方法句柄所對應的類沒有進行初始化,則須要先出發其初始化。數據結構
2.被動引用的幾種經典的場景多線程
1)經過子類引用父類的靜態字段,不會致使子類初始化oop
2)經過數組定義來引用類,不會觸發此類的初始化線程
3)常量在編譯階段會存入調用類的常量池中,本質上並無直接引用到定義量的類,所以不會觸發定義常量的類初始化。
3、類加載的過程
一、加載(Loading) 在加載階段(能夠參考java.lang.ClassLoader的loadClass()方法),虛擬機須要完成如下三件事情: (1). 經過一個類的全限定名來獲取定義此類的二進制字節流(並無指明要從一個Class文件中獲取,能夠從其餘渠道,譬如:網絡、動態生成、數據庫等); (2). 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構; (3). 在內存中(對於HotSpot虛擬就而言就是方法區)生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口; 加載階段和鏈接階段(Linking)的部份內容(如一部分字節碼文件格式驗證動做)是交叉進行的,加載階段還沒有完成,鏈接階段可能已經開始,但這些夾在加載階段之中進行的動做,仍然屬於鏈接階段的內容,這兩個階段的開始時間仍然保持着固定的前後順序。 特別地,第一件事情(經過一個類的全限定名來獲取定義此類的二進制字節流)是由類加載器完成的,具體涉及JVM預約義的類加載器、雙親委派模型等內容,詳情請參見個人轉載博文《深刻理解Java類加載器(一):Java類加載原理解析》中的說明,此不贅述。二、驗證(Verification) 驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。 驗證階段大體會完成4個階段的檢驗動做:文件格式驗證:驗證字節流是否符合Class文件格式的規範(例如,是否以魔術0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型)元數據驗證:對字節碼描述的信息進行語義分析,以保證其描述的信息符合Java語言規範的要求(例如:這個類是否有父類,除了java.lang.Object以外);字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的;符號引用驗證:確保解析動做能正確執行。 驗證階段是很是重要的,但不是必須的,它對程序運行期沒有影響。若是所引用的類通過反覆驗證,那麼能夠考慮採用-Xverifynone參數來關閉大部分的類驗證措施,以縮短虛擬機類加載的時間。三、準備(Preparation) 準備階段是正式爲類變量(static 成員變量)分配內存並設置類變量初始值(零值)的階段,這些變量所使用的內存都將在方法區中進行分配。這時候進行內存分配的僅包括類變量,而不包括實例變量,實例變量將會在對象實例化時隨着對象一塊兒分配在堆中。其次,這裏所說的初始值「一般狀況」下是數據類型的零值,假設一個類變量的定義爲:public static int value = 123; 那麼,變量value在準備階段事後的值爲0而不是123。由於這時候還沒有開始執行任何java方法,而把value賦值爲123的putstatic指令是程序被編譯後,存放於類構造器方法<clinit>()之中,因此把value賦值爲123的動做將在初始化階段纔會執行。至於「特殊狀況」是指:當類字段的字段屬性是ConstantValue時,會在準備階段初始化爲指定的值,因此標註爲final以後,value的值在準備階段初始化爲123而非0。 public static final int value = 123;四、解析(Resolution) 解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。解析動做主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符7類符號引用進行。五、初始化(Initialization) 類初始化階段是類加載過程的最後一步。在前面的類加載過程當中,除了在加載階段用戶應用程序能夠經過自定義類加載器參與以外,其他動做徹底由虛擬機主導和控制。到了初始化階段,才真正開始執行類中定義的java程序代碼(字節碼)。 在準備階段,變量已經賦過一次系統要求的初始值(零值);而在初始化階段,則根據程序猿經過程序制定的主觀計劃去初始化類變量和其餘資源,或者更直接地說:初始化階段是執行類構造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的全部類變量的賦值動做和靜態語句塊static{}中的語句合併產生的,編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態語句塊只能訪問到定義在靜態語句塊以前的變量,定義在它以後的變量,在前面的靜態語句塊能夠賦值,可是不能訪問。以下:public class Test{static{i=0;System.out.println(i);//Error:Cannot reference a field before it is defined(非法向前應用)}static int i=1;} 那麼註釋報錯的那行代碼,改爲下面情形,程序就能夠編譯經過並能夠正常運行了。public class Test{static{i=0;//System.out.println(i);}static int i=1;public static void main(String args[]){System.out.println(i);}}/* Output:1*///:~ 類構造器<clinit>()與實例構造器<init>()不一樣,它不須要程序員進行顯式調用,虛擬機會保證在子類類構造器<clinit>()執行以前,父類的類構造<clinit>()執行完畢。因爲父類的構造器<clinit>()先執行,也就意味着父類中定義的靜態語句塊/靜態變量的初始化要優先於子類的靜態語句塊/靜態變量的初始化執行。特別地,類構造器<clinit>()對於類或者接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對類變量的賦值操做,那麼編譯器能夠不爲這個類生產類構造器<clinit>()。 虛擬機會保證一個類的類構造器<clinit>()在多線程環境中被正確的加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的類構造器<clinit>(),其餘線程都須要阻塞等待,直到活動線程執行<clinit>()方法完畢。特別須要注意的是,在這種情形下,其餘線程雖然會被阻塞,但若是執行<clinit>()方法的那條線程退出後,其餘線程在喚醒以後不會再次進入/執行<clinit>()方法,由於 在同一個類加載器下,一個類型只會被初始化一次。若是在一個類的<clinit>()方法中有耗時很長的操做,就可能形成多個線程阻塞,在實際應用中這種阻塞每每是隱藏的,以下所示:public class DealLoopTest {static{System.out.println("DealLoopTest...");}static class DeadLoopClass {static {if (true) {System.out.println(Thread.currentThread()+ "init DeadLoopClass");while (true) { // 模擬耗時很長的操做}}}}public static void main(String[] args) {Runnable script = new Runnable() { // 匿名內部類public void run() {System.out.println(Thread.currentThread() + " start");DeadLoopClass dlc = new DeadLoopClass();System.out.println(Thread.currentThread() + " run over");}};Thread thread1 = new Thread(script);Thread thread2 = new Thread(script);thread1.start();thread2.start();}}/* Output:DealLoopTest...Thread[Thread-1,5,main] startThread[Thread-0,5,main] startThread[Thread-1,5,main]init DeadLoopClass*///:~ 如上述代碼所示,在初始化DeadLoopClass類時,線程Thread-1獲得執行並在執行這個類的類構造器<clinit>() 時,因爲該方法包含一個死循環,所以久久不能退出。