java類的加載與初始化總結 JAVA類加載和初始化 Java系列筆記(1) - Java 類加載與初始化

一、觸發類加載的緣由(主動調用與被動調用):

    六種主動調用: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 }
View Code

 

二、‘單例模式’中靜態成員初始化問題

 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 }
View Code

 

三、構造方法內的多態對初始化的影響

 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 }
View Code

 

 

總結:類的加載與初始化順序上面已經總結了。但實際判斷時任然須要謹慎。

   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),

      這不就是「初始化」的意思嘛?而「執行賦值表達式」更像是用戶根據本身的程序須要設置自定義的初始值,而不是分配內存空間後的默認值(這應該就是一般意義的「初始化」了吧)。而這個設置自定義初始值的行爲,

      便可以是在變量的定義處,也能夠是在構造方法中,或者在須要時刻的方法調用中(惰性初始化)。而這種設置自定義初始值的行爲的正確保證,就是上面總結的「類的加載與初始化順序」的嚴格順序執行。

 

參考:java中類的加載順序介紹(ClassLoader)

          JAVA類加載和初始化

         Java系列筆記(1) - Java 類加載與初始化

相關文章
相關標籤/搜索