在創建對象之前,首先要判斷類有沒有被加載,例如創建對象或調用類的static方法變量時,會觸發類加載,如下:
Dog dog = new Dog();
首次訪問某個類的靜態方法或者靜態字段時:
Dog.staticFields;
類加載機制
java是使用雙親委派模型來進行類的加載的,所以在描述類加載過程前,我們先看一下它的工作過程:
雙親委托模型的工作過程是:如果一個類加載器(ClassLoader)收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委托給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都應該傳送到頂層的啟動類加載器中,只有當父類加載器反饋自己無法完成這個加載請求(它的搜索范圍中沒有找到所需要加載的類)時,子加載器才會嘗試自己去加載。
使用雙親委托機制的好處是:能夠有效確保一個類的全局唯一性,當程序中出現多個限定名相同的類時,類加載器在執行加載時,始終只會加載其中的某一個類。
1、加載
由類加載器負責根據一個類的全限定名來讀取此類的二進制字節流到JVM內部,並存儲在運行時內存區的方法區,然后將其轉換為一個與目標類型對應的java.lang.Class對象實例
2、驗證
格式驗證:驗證是否符合class文件規范
語義驗證:檢查一個被標記為final的類型是否包含子類;檢查一個類中的final方法是否被子類進行重寫;確保父類和子類之間沒有不兼容的一些方法聲明(比如方法簽名相同,但方法的返回值不同)
操作驗證:在操作數棧中的數據必須進行正確的操作,對常量池中的各種符號引用執行驗證(通常在解析階段執行,檢查是否可以通過符號引用中描述的全限定名定位到指定類型上,以及類成員信息的訪問修飾符是否允許訪問等)
3、准備
為類中的所有靜態變量分配內存空間,並為其設置一個初始值(由於還沒有產生對象,實例變量不在此操作范圍內)
被final修飾的static變量(常量),會直接賦值;
4、解析
將常量池中的符號引用轉為直接引用(得到類或者字段、方法在內存中的指針或者偏移量,以便直接調用該方法),這個可以在初始化之后再執行。
解析需要靜態綁定的內容。 // 所有不會被重寫的方法和域都會被靜態綁定
以上2、3、4三個階段又合稱為鏈接階段,鏈接階段要做的是將加載到JVM中的二進制字節流的類數據信息合並到JVM的運行時狀態中。
5、初始化(先父后子)
5.1 為靜態變量賦值
5.2 執行static代碼塊
注意:static代碼塊只有jvm能夠調用
如果是多線程需要同時初始化一個類,僅僅只能允許其中一個線程對其執行初始化操作,其余線程必須等待,只有在活動線程執行完對類的初始化操作之后,才會通知正在等待的其他線程。
因為子類存在對父類的依賴,所以類的加載順序是先加載父類后加載子類,初始化也一樣。不過,父類初始化時,子類靜態變量的值也有有的,是默認值。
最終,方法區會存儲當前類類信息,包括類的靜態變量、類初始化代碼(定義靜態變量時的賦值語句 和 靜態初始化代碼塊)、實例變量定義、實例初始化代碼(定義實例變量時的賦值語句實例代碼塊和構造方法)和實例方法,還有父類的類信息引用。
創建對象
類沒有父類的情況(父類為Object不算,因為Object是所有類的父類)
1.第一次創建 Dog 對象先執行上面的類加載階段
2.在堆上為 Dog 對象分配足夠的存儲空間,所有屬性和方法都被設置成默認值(數字為 0,字符為 null,布爾為 false,而所有引用被設置成 null)
3.執行構造函數檢查是否有父類,如果有父類會先調用父類的構造函數,這里假設 Dog 沒有父類,執行默認值字段的賦值即方法的初始化動作。
4.執行構造函數。
有父類的情況
假設:
Dog extends Animal
1、執行第一步,找出 Dog.class 文件,接着在加載過程中發現他有一個基類(通過 extends 關鍵字),於是先執行 Animal 類的第一二步,加載其靜態變量和方法,加載結束之后再加載子類 Dog 的靜態變量和方法。
如果 Animal 類還有父類就以此類推,最終的基類叫做根基類。
注意:因為子類的 static 初始化可能會依賴於父類的靜態資源,所以要先加載父類的靜態資源。
2、接着要 new Dog 對象,先為 Dog 對象分配存儲空間 -> 到 Dog 的構造函數 -> 創建默認的屬性。這里其構造函數里面的第一行有個隱含的 super(),即父類構造函數,所以這時會跳轉到父類 Animal 的構造函數。
Java 會幫我們完成構造函數的補充,Dog 實際隱式的構造函數如下:
Dog() {
//創建默認屬性和方法(成員變量等) //調用父類的構造函數super()(可顯式寫出) //對默認屬性和方法分別進行賦值和初始化 }
3、父類 Animal 執行構造函數前也是分配存儲空間 -> 到其構造函數 -> 創建默認的屬性 -> 發現挖槽我已經沒有父類了,這個時候就給它的默認的屬性賦值和方法的初始化。
4、接着執行構造函數余下的部分,結束后跳轉到子類 Dog 的構造函數。
5、子類 Dog 對默認屬性和方法分別進行賦值和初始化,接着完成構造函數接下來的部分。
因此,對於構造器有這樣的規定:
- 子類中所有的構造器默認都會訪問父類中空參數的構造器
- 當父類中沒有空參數的構造器時,子類的構造器必須通過this(參數列表)或者super(參數列表)語句指定調用本類或者父類中相應的構造器。同時,只能”二選一”,且必須放在構造器的首行
- 如果子類構造器中既未顯式調用父類或本類的構造器,且父類中又沒有無參的構造器,則編譯出錯
問題
1、為什么要執行父類 Animal 的構造方法才繼續子類 Dog 的屬性及方法賦值?
因為子類 Dog 的非靜態變量和方法的初始化有可能使用到其父類 Animal 的屬性或方法,所以子類構造默認的屬性和方法之后不應該進行賦值,而要跳轉到父類的構造方法完成父類對象的構造之后,才來對自己的屬性和方法進行初始化。
這也是為什么子類的構造函數顯示調用父類構造函數 super() 時要強制寫在第一行的原因,程序需要跳轉到父類構造函數完成父類對象的構造后才能執行子類構造函數的余下部分。
2、為什么對屬性和方法初始化之后再執行構造函數其他的部分?
因為構造函數中的顯式部分有可能使用到對象的屬性和方法。
Tips:其實這種初始化過程都是為了保證后面資源初始化用到的東西前面的已經初始化完畢了。
示例
//父類Animal class Animal { /* 8、執行初始化 */ private int i = 9; protected int j; /* 7、調用構造方法,創建默認屬性和方法,完成后發現自己沒有父類 */ public Animal() { /* 9、執行構造方法剩下的內容,結束后回到子類構造函數中 */ System.out.println("i = " + i + ", j = " + j); j = 39; } /* 2、初始化根基類的靜態對象和靜態方法 */ private static int x1 = print("static Animal.x1 initialized"); static int print(String s) { System.out.println(s); return 47; } } //子類 Dog public class Dog extends Animal { /* 10、初始化默認的屬性和方法 */ private int k = print("Dog.k initialized"); /* * 6、開始創建對象,即分配存儲空間->創建默認的屬性和方法。 遇到隱式或者顯式寫出的super()跳轉到父類Animal的構造函數。 * super()要寫在構造函數第一行 */ public Dog() { /* 11、初始化結束執行剩下的語句 */ System.out.println("k = " + k); System.out.println("j = " + j); } /* 3、初始化子類的靜態對象靜態方法,當然mian函數也是靜態方法 */ private static int x2 = print("static Dog.x2 initialized"); /* * 1、要執行靜態main,首先要加載Dog.class文件,加載過程中發現有父類Animal, * 所以也要加載Animal.class文件,直至找到根基類,這里就是Animal */ public static void main(String[] args) { /* 4、前面步驟完成后執行main方法,輸出語句 */ System.out.println("Dog constructor"); /* 5、遇到new Dog(),調用Dog對象的構造函數 */ Dog dog = new Dog(); /* 12、運行main函數余下的部分程序 */ System.out.println("Main Left"); } }
打印結果為:
static Animal.x1 initialized static Dog.x2 initialized Dog constructor i = 9, j = 0 Dog.k initialized k = 47 j = 39 Main Left
