Java核心 -- Class類

導論

在周志明的《深入理解Java虛擬機》書中的類加載機制章節上有提到,在“類加載”過程的加載階段,虛擬機需要完成以下三件事:

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流;
  2. 將這個字節流所代表的靜態存儲結構轉化爲方法區的運行時數據結構;
  3. 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法區這個類的各種數據的訪問入口

 

在《Java核心技術 卷一》關於反射的內容中提到:

    在程序運行期間,Java運行時系統始終爲所有的對象維護一個被稱爲運行時的類型標識。這個信息跟蹤着每個對象所屬的類,虛擬機利用運行時類型信息選擇相應的方法執行。而保存這些信息的類被稱爲Class

 

Class類簡介

在java世界裏,一切皆對象。從某種意義上來說,java有兩種對象:實例對象和Class對象。每個類的運行時的類型信息就是用Class對象表示的。它包含了與類有關的信息。其實我們的實例對象就通過Class對象來創建的。Java使用Class對象執行其RTTI(運行時類型識別,Run-Time Type Identification),多態是基於RTTI實現的。

  每一個類都有一個Class對象,每當編譯一個新類就產生一個Class對象,基本類型 (boolean, byte, char, short, int, long, float, and double)有Class對象,數組有Class對象,就連關鍵字void也有Class對象(void.class)。Class對象對應着java.lang.Class類,如果說類是對象抽象和集合的話,那麼Class類就是對類的抽象和集合。

  Class類沒有公共的構造方法,Class對象是在類加載的時候由Java虛擬機以及通過調用類加載器中的 defineClass 方法自動構造的,因此不能顯式地聲明一個Class對象。一個類被加載到內存並供我們使用需要經歷如下三個階段:

  1. 加載,這是由類加載器(ClassLoader)執行的。通過一個類的全限定名來獲取其定義的二進制字節流(Class字節碼),將這個字節流所代表的靜態存儲結構轉化爲方法去的運行時數據接口,根據字節碼在java堆中生成一個代表這個類的java.lang.Class對象
  2. 鏈接。在鏈接階段將驗證Class文件中的字節流包含的信息是否符合當前虛擬機的要求,爲靜態域分配存儲空間並設置類變量的初始值(默認的零值),並且如果必需的話,將常量池中的符號引用轉化爲直接引用。
  3. 初始化。到了此階段,才真正開始執行類中定義的java程序代碼。用於執行該類的靜態初始器和靜態初始塊,如果該類有父類的話,則優先對其父類進行初始化。

所有的類都是在對其第一次使用時,動態加載到JVM中的(懶加載)。當程序創建第一個對類的靜態成員的引用時,就會加載這個類。使用new創建類對象的時候也會被當作對類的靜態成員的引用。因此java程序程序在它開始運行之前並非被完全加載,其各個類都是在必需時才加載的。這一點與許多傳統語言都不同。動態加載使能的行爲,在諸如C++這樣的靜態加載語言中是很難或者根本不可能複製的。

  在類加載階段,類加載器首先檢查這個類的Class對象是否已經被加載。如果尚未加載,默認的類加載器就會根據類的全限定名查找.class文件。在這個類的字節碼被加載時,它們會接受驗證,以確保其沒有被破壞,並且不包含不良java代碼。一旦某個類的Class對象被載入內存,我們就可以它來創建這個類的所有對象。
 

 

虛擬機爲每個類創建唯一的Class對象

一旦類被加載了到了內存中,那麼不論通過哪種方式獲得該類的Class對象,它們返回的都是指向同一個java堆地址上的Class引用。jvm不會創建兩個相同類型的Class對象。

其實對於任意一個Class對象,都需要由它的類加載器和這個類本身一同確定其在就Java虛擬機中的唯一性,也就是說,即使兩個Class對象來源於同一個Class文件,只要加載它們的類加載器不同,那這兩個Class對象就必定不相等。這裏的“相等”包括了代表類的Class對象的equals()、isAssignableFrom()、isInstance()等方法的返回結果,也包括了使用instanceof關鍵字對對象所屬關係的判定結果。所以在java虛擬機中使用雙親委派模型來組織類加載器之間的關係,來保證Class對象的唯一性。
--------------------- 
作者:mcryeasy 
來源:CSDN 
原文:https://blog.csdn.net/mcryeasy/article/details/52344729 
版權聲明:本文爲博主原創文章,轉載請附上博文鏈接!

JVM爲每個加載過的類生成一個Class類的對象實例,比如Student類被加載後,JVM就生成一個和Stundent信息相對應的Class對象,不論是通過Student.class還是通過student.getClass()獲取該對象,都是獲取同一個Class對象實例的引用。也就是說,對於一個加載到虛擬機中的類,不論對其創建了多少個實例,內存中都有且只有一個與之相對應的Class對象。

public class Student {

    public static void main (String[] args) {

        Class c0 = Student.class;
        Student s1 = new Student();
        Student s2 = new Student();
        Class c1 = s1.getClass();
        Class c2 = s2.getClass();

        // Student的實例對象1:
        System.out.println(s1.hashCode());
        // Student的實例對象1:
        System.out.println(s2.hashCode());
        System.out.println();
        // Student類未被創建任何實例之前獲取的Class對象(僅被加載到虛擬機):
        System.out.println(c0.hashCode());
        // Student的實例對象1獲取的Class對象:
        System.out.println(c1.hashCode());
        // Student的實例對象2獲取的Class對象:
        System.out.println(c2.hashCode());
    }
}

輸出結果:
1163157884
1956725890

460141958
460141958
460141958


分析結果可知,同一個類的不同對象實例獲取的對應的Class對象是同一個引用,而這個Class對象是該類被虛擬機加載時,就創建至內存中。

 

Class類不能被繼承,只能由虛擬機創建實例

Class類也只是Java.lang包下的一個普通的類,下圖是Class類源碼的截圖,從圖中可以知道:

  • Class類是在java.lang包下的一個類(其父類也是 java.lang.Object);
  • Class類由final修飾,不允許被子類繼承;
  • Class類的構造函數是private修飾符,意味着,Class類只能由虛擬機創建實例對象;

 

獲取類的Class對象引用的三種方式

  • 通過調用類對象的getClass()方法,該方法是繼承自Object類:getClass()是Object的方法,返回class對象;
  • 通過調用Class類的forName()類靜態方法,方法入參必須有類的全限定類名:forName()是Class類的方法,返回class對象;
  • 通過類字面常量:類.class返回class對象;
// Person是一個抽象類,含有靜態域
public abstract class Person {

    static {
        System.out.println("初始化。。。");
    }

}


// Student中含有main主函數,對比演示通過類字面量class和Class.forName()獲取Class對象的不同
public class Student {

    public static void main (String[] args) {

        // 通過類字面量獲取Class對象
        Class c1 = Person.class;
        System.out.println("類字面量演示:\n" + c1.hashCode() + "\n");

        // 通過Class.forName()類靜態方法獲取Class對象
        Class c2 = null;
        try {
             c2 = Class.forName("Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("Class.forName()靜態方法演示:\n" + c2.hashCode());
    }
}

輸出結果:
類字面量演示:
1163157884

初始化。。。
Class.forName()靜態方法演示:
1163157884

分析結果:通過類字面量獲取Class對象不會觸發類初始化,而Class.forName()會在類加載的初始化階段完成後纔會被調用

 

JVM在運行Java程序時,不是一次性加載所有的類,而是按需加載,JVM的類加載過程可以分爲如下7個階段:

其中在加載階段(Loading),在本文的導論部分有介紹,加載階段會爲類創建相應的Class對象;而初始化階段是屬於類加載的最後階段,會對類的靜態變量進行初始化。所以對比上述實驗結果,可以得知通過類字面量class獲取類的Class對象,會觸發類加載完成Loading階段,但是不會觸發Initialization階段,因爲實驗中沒有打印“初始化。。。”,而通過Class.forName()方法則會觸發Initialization階段的執行。並且由於Class.forName()方法在虛擬機的編譯期無法檢測其傳遞的類全限定名字符串對應的類是否存在,只能在程序運行時進行檢查,所以方法在使用時需要捕獲一個名稱爲ClassNotFoundException的異常。

 

將上述實驗中對Person類的Class對象獲取,修改成對Student類(該類中含有main入口函數)的Class對象的獲取,並且在Student類中也增添靜態域,實驗如下:

public class Student {

    static {
        System.out.println("初始化。。。");
    }
    
    public static void main (String[] args) {

        // 通過類字面量獲取Class對象
        Class c1 = Student.class;
        System.out.println("類字面量演示:\n" + c1.hashCode() + "\n");

        // 通過Class.forName()類靜態方法獲取Class對象
        Class c2 = null;
        try {
             c2 = Class.forName("Student");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("Class.forName()靜態方法演示:\n" + c2.hashCode());
    }
}


輸出結果:
初始化。。。
類字面量演示:
460141958

Class.forName()靜態方法演示:
460141958

分析結果:main方法在執行函數體之前,靜態域就已經被執行,也就是說在Student.class執行之前,Student就已經完成了類加載的初始化階段。

爲什麼對Person類和Student類分別進行試驗,會有不一樣的現象?答案是,當Java虛擬機啓動時,用戶需要指定一個要執行的主類(包含main方法的類),虛擬機會先初始化這個主類,由於Student類中包含有main方法,所以即便main方法中沒有任何關於Student類的代碼,Student類都已經完成了類加載的初始化階段。相反的,Person不是程序的主類,不會被事先觸發類加載的過程。

因此,對比獲取類的Class對象的三種方式,通過getClass()和forName()方法的方式都需要類被初始化,而類字面量的方式則更簡單且安全,因爲它在編譯時就會接受檢查(因此不需要置於try語句塊中),不會自動初始化該類,更加有趣的是字面常量的獲取Class對象引用方式不僅可以應用於普通的類,也可以應用用接口,數組以及基本數據類型,這點在反射技術應用傳遞參數時很有幫助。

什麼情況下需要開始類加載過程的第一個階段:加載?Java虛擬機規範中並沒有進行強制約束,這點可以交給虛擬機的具體實現來自由把握。但是對於初始化階段,虛擬機規範則是嚴格規定了有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準備自然需要在此之前開始):

  1. 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
  2. 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
  3. 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
  4. 當虛擬機啓動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  5. 當使用JDK 1.7的動態語言支持時,如果一個java.lang.invoke.MethodHandle實例最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,並且這個方法句柄所對應的類沒有進行過初始化,則需要先觸發其初始化。

對於這5種會觸發類進行初始化的場景,虛擬機規範中使用了一個很強烈的限定語:“有且只有”,這5種場景中的行爲稱爲對一個類進行主動引用。除此之外,所有引用類的方式都不會觸發初始化,稱爲被動引用。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章