六種主動調用:html
1)、建立類的實例(new操做、反射、cloning、反序列化)java
2)、調用類的靜態方法c++
3)、使用或對類/接口的static屬性賦值(不包括static final的與在編譯期肯定的常量表達式(包括常量、字符串常量))數組
4)、調用API中的反射方法,Class.forName()等。數據結構
5)、子類被初始化ide
6)、被設定爲JVM啓動時的啓動類(含main方法的主類)post
其它都爲被動引用:被動引用不會觸發類的初始化操做(只會加載、連接),如僅申明一個類的引用、經過數組定義引用類等。url
加載、連接(驗證、準備、解析)、初始化、使用、卸載spa
1)、加載.net
i)、java編譯器加載類的二進制字節流文件(.class文件),若是該類有基類,向上一直加載到根基類(無論基類是否使用都會加載)。
ii)、將二進制字節碼加載到內存,解析成方法區對應的數據結構。
iii)、在java邏輯堆中生成該類的java.lang.Class對象,做爲方法區中該類的入口。
類加載器:分默認加載器和用戶自定義加載器
Bootstrap ClassLoader:頂層加載器,由c++實現。負責JVM啓動時加載JDK核心類庫以及加載後面兩個類加載器。
Extension ClassLoader:繼承自ClassLoader的類,負責加載{JAVA_HOME}/jre/lib/ext目錄下的全部jar包。
App ClassLoader:上面加載器的子對象,負責加載應用程序CLASSPATH目錄下的class文件和jar包。
Customer ClassLoader:用戶繼承自ClassLoader類的自定義加載器,用來處理特殊加載需求。如Tomcat等都有本身實現的加載器。
類加載器採用雙親委託(自底向上查詢)來避免重複加載類,而加載順序倒是自頂向下的。
2)、連接
i)、驗證:字節碼完整性、final類與方法、方法簽名等的檢查驗證。
ii)、準備:爲靜態變量分配存儲空間(內存單元全置0,即基本類型爲默認值,引用類型爲null)。
iii)、解析(這步是可選的):將常量池內的符號引用替換爲直接引用。
類的加載和連接只執行一次,故static成員也只加載一次,做爲類所擁有、類的全部實例共享。
3)、初始化
包括類的初始化、對象的初始化。
類的初始化:
初始化靜態字段(執行定義處的賦值表達式)、執行靜態初始化塊。
注:有父類則先遞歸的初始化父類的。
對象的初始化:
若是須要建立對象,則會執行建立對象並初始化:
i)、在堆上爲建立的對象分配足夠的存儲空間,並將存儲單元清零,即基本類型爲默認值,引用類型爲null。
i)、初始化非靜態成員變量(即執行變量定義處的賦值表達式)。
ii)、執行構造方法。
注:若是有父類,則先遞歸的初始化父類成員,最後纔是本類。
4)、使用
5)、卸載
對象的引用(棧中)在超出做用域後被系統當即回收;對象自己(堆中)被gc標記爲垃圾,在gc下次執行垃圾處理時被回收。
一個類最早初始化static變量和static塊;
而後分配該類以及父類的成員變量的內存空間,再賦值初始化,最後調用構造方法;
在父類與子類之間,老是優先建立、初始化父類。
即:(靜態變量、靜態初始化塊)–>(變量、初始化塊)–> 構造器,其中基類老是優先於子類的。
詳細分析例題:
一、static靜態成員初始化細節
1 public class Test8 { 2 3 public static void main(String[] args) { 4 System.out.println(Super.a); 5 System.out.println(Super.b); 6 System.out.println(Super.bb); 7 System.out.println(Super.c); 8 System.out.println(new Super("cc").c); 9 //對於「初始化」的意思,應該包括初始化和執行賦值表達式(若是有的話)。例如static成員的初始化實際上包括兩步:準備階段(JVM連接)中的內存分配(全置0,即基本類型成默認值,引用類型爲null)和初始化中的static成員賦初值操做(即若是有的話,執行static字段定義處的賦值表達式) 10 //對於a、b 由於有賦初值的表達式,故會獲得自定義的初始值。對於c則採用準備階段的null。 11 //只有建立對象纔會調用構造方法(執行構造方法中的動做)。b的值被從新設置。 12 13 Super sup2 = new Super("ccc"); //至關於每次新建對象都對'實例所共享、類全部的'b從新設值(是從新給b賦值,不是新建)。 14 System.out.print(Super.c); 15 } 16 17 } 18 19 20 class Super{ 21 static String a; 22 static String b = getB(); 23 static String bb = getC(); 24 static String c = "c"; 25 26 Super(String s){ 27 c = s; 28 } 29 30 static String getC(){ 31 return c; 32 } 33 34 static String getB(){ 35 return "b"; 36 } 37 }
二、‘單例模式’中靜態成員初始化問題
1 public class Test9 { 2 3 public static void main(String[] args) { 4 System.out.println(Single.b); 5 System.out.println(Single.c); 6 //在Single類加載的連接階段靜態字段都置默認值(基本類型爲默認,引用爲null),因此sin、b、c首先爲null、0、0。 7 //而後按照定義的順序執行初始化賦值,先執行sin的賦值,由於用new建立對象,因此會執行構造方法,而後b=1,c=1。這時由於static字段只加載一次,因此b、c只是作賦值操做(有賦值表達式的話),因此b沒操做,c從新賦值爲0。 8 9 10 //那麼,若是交換靜態字段sin和c的位置,上面輸出? 11 } 12 13 } 14 15 16 class Single{ 17 private static Single sin = new Single(); 18 public static int b; 19 public static int c = 0; 20 21 private Single(){ 22 b++; 23 c++; 24 } 25 26 public Single getInstance(){ 27 return sin; 28 } 29 }
三、構造方法內的多態對初始化的影響
1 public class Test10 { 2 3 public static void main(String[] args) { 4 new SubTest(); 5 6 //輸出爲:SuperTest() before draw() 7 // SubTest() ,i = 0 8 // SuperTest() after draw() 9 // SubTest() ,i = 1 10 11 //分析:由於沒有靜態成員,因此在用new建立子類對象時,先在堆中爲該對象分配足夠空間(內存空間全置二進制的0,即基本類型爲默認值,引用類型爲null), 12 //而後,調用父類構造方法(有實例變量會先初始化實例變量),但draw()調用的是子類的重寫方法,那麼問題是,這時候子類實例變量i只分配了內存空間(默認值爲0), 13 //尚未初始化,因此輸出的i爲0。直到子類初始化實例變量時,i才被賦值爲1,最後執行子類的構造方法,因此輸出i爲1。 14 } 15 16 } 17 18 19 class SuperTest{ 20 SuperTest(){ 21 System.out.println("SuperTest() before draw()"); 22 draw(); //調用子類的重寫方法(多態) 23 System.out.println("SuperTest() after draw()"); 24 } 25 void draw(){ 26 System.out.println("super draw"); 27 } 28 29 } 30 31 class SubTest extends SuperTest{ 32 private int i = 1; 33 SubTest(){ 34 System.out.println("SubTest() ,i = "+i); } 35 @Override 36 void draw(){ 37 System.out.println("SubTest() ,i = "+i); 38 } 39 }
總結:類的加載與初始化順序上面已經總結了。但實際判斷時任然須要謹慎。
i)、對於不少書上說的和你們掛在嘴邊的「初始化」一詞,如‘初始化‘靜態變量、‘初始化’實例變量。這裏的初始化個人更細入的理解是,‘初始化’包括「分配內存空間」和「執行賦值表達式」兩步。
ii)、「分配內存空間」,即將獲取到的內存單元所有置爲二進制的0(對於基本類型天然就是默認值,對於引用類型都爲null),而這一步是無論變量定義處的賦值表達式的。如int a ; int b =1; 在這一步都是同樣置爲二進制的0的。
「執行賦值表達式」,便是在變量「分配內存空間」後對變量的賦值操做。如 int a;int b =1; 在這一步a沒有賦值操做,b就有賦值操做了,而後a依然仍是分配內存空間後的默認值,而b就從新賦值爲1了。
iii)、「初始化」即先分配內存空間,再對變量執行賦值表達式(若是有的話)。這樣分前後的意義保證了對變量的賦值前,變量已經獲取到了正確的初始內存空間。如static變量的初始化,實際上在’準備階段‘就分配好內存單元,
在’初始化階段‘的第一步才執行定義處的賦值表達式。這就是例一中考察的重點,在分配內存空間後與執行定義處的賦值操做後獲得的值不同。又如實例變量的初始化,他的所謂「初始化」也是分兩個階段的,不過他的兩個
階段間相隔的操做很少,因此看成一個概念一般不會出問題,但遇到例三的狀況就出問題了。參考《Thinking In Java》中的建議就是「儘可能在構造方法中慎用非final或private(隱式爲final)方法」
iiii)、對於個人理解把「初始化」細化爲「分配內存空間」和「執行賦值表達式」兩步,其實也挺糾結的。’分配內存空間’即包括內存空間的初始分配,而後變量也天然獲得初始值了(對於基本類型天然就是默認值,對於引用類型都爲null),
這不就是「初始化」的意思嘛?而「執行賦值表達式」更像是用戶根據本身的程序須要設置自定義的初始值,而不是分配內存空間後的默認值(這應該就是一般意義的「初始化」了吧)。而這個設置自定義初始值的行爲,
便可以是在變量的定義處,也能夠是在構造方法中,或者在須要時刻的方法調用中(惰性初始化)。而這種設置自定義初始值的行爲的正確保證,就是上面總結的「類的加載與初始化順序」的嚴格順序執行。