1. 程式人生 > >Java原始碼分析——Class類、ClassLoader類解析(一) 類的抽象與獲取

Java原始碼分析——Class類、ClassLoader類解析(一) 類的抽象與獲取

    Class類是集合了所有類的屬性、行為的抽象,描述類的修飾、類的構造器、類的欄位以及類的方法等抽象,這裡的類是指廣泛的類,包括了介面、註解、陣列等。簡單的來說,它涵蓋了所有類的共性,所以研究它時,應當從所有類的共性出發,來探討其中的內容。而ClassLoader類則是負責了類的載入這一塊,負責將Class類物件從jvm中加載出來。

    雖然Class類是所有類的抽象,但是它依舊是與其它類一樣具有共同性,建立一個自定義Test類,經過編譯會產生一個Test.class位元組碼檔案,該位元組碼檔案儲存著Test類的抽象,也就是儲存著Test類的各種型別與方法等,jvm會通過它來建立Test類對應的Class類物件,jvm再通過該Class類物件來建立Test例項。不管建立多少個例項,位元組碼檔案始終只有一個,且所有的例項都依賴於該Class物件。這也是所有類的hashcode都有唯一性的一個原因。

    當我們new一個新物件或者引用靜態成員變數時,JVM中的類載入器子系統會將對應Class類物件載入到JVM中,這個過程也就是通過類載入器將位元組碼檔案轉化為Class類物件,然後JVM再根據這個型別資訊相關的Class類物件建立我們需要例項物件或者提供靜態變數的引用值。但在實際中中,Class類物件是不能被外部建立的,它只能從jvm中載入其物件,因為只有私有構造方法,必須通過forName與getClass方法來獲取,所以不會存在Class.class檔案,但會存在其它類的位元組碼檔案:

private Class(ClassLoader loader)
{ classLoader = loader; }

    那麼如何來載入一個Class類物件或者獲取一個Class類物件的引用呢?在其內部提供了forName方法:

@CallerSensitive
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass
(); return forName0(className, true, ClassLoader.getClassLoader(caller), caller); }

    該方法通過Reflection反射類的getCallerClass本地方法來獲取當前類的Class物件的引用,但這個Class類物件是隻有標明是屬於哪一個類的,但裡面並沒有載入進對應類的資訊。後呼叫forName0本地方法來對Class類物件進行初始化載入,載入對應類的靜態程式碼塊與靜態屬性以及執行靜態方法(構造方法除外),也就是說getCallerClass獲取其Class物件時並沒有對類進行初始化,先用另外一個forName方法測試,這個方法可以手動開關forName0方法的初始化,如下程式碼所示:

class Kt{
    static{
        System.out.println("載入了3。。。。");
    }
    {
        System.out.println("載入了。。。。");
    }
    public Kt(){
        System.out.println("載入了2。。。。");
    }
    public void gg() throws IllegalAccessException, InstantiationException {

        Reflection.getCallerClass(1);
    }
}

public class Test {
    public static void main(String args[]) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        System.out.println("未開啟初始化。。。。。。");
        Class.forName("test.Kt",false,Kt.class.getClassLoader());
        System.out.println("開啟初始化。。。。。。");
        System.out.println("開啟初始化。。。。。。");
        Class.forName("test.Kt",true,Kt.class.getClassLoader());
    }
}

在這裡插入圖片描述
    這裡用getCallerClass(int )來代替getCallerClass(),兩者效果是一樣的,證明了getCallerClass只是獲取其對應Class類物件,但是並沒有對類進行初始化載入載入。對forName0方法的解釋,官方的文件寫到:返回與具有給定字串名稱的類或介面關聯的Class物件,使用給定的類載入器載入。給定類或介面的完全限定名稱(以getName返回的相同格式)此方法嘗試查詢,載入和連結類或介面。指定的類載入器用於載入類或介面。如果引數載入器為null,則通過根類載入器載入該類。 僅當initialize引數為true且之前尚未初始化時,才會初始化該類。

 private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;

    另外一個forName方法可以讓呼叫者手動載入靜態屬性與方法,也就是上面驗證程式碼用到的,裡面加入了java的安全管理器,如果是系統級別的載入器則不需要驗證許可權,否則就需要驗證載入器是否有載入該位元組碼檔案的許可權:

 /**
     * @param name 類的完全限定名
     * @param initialize 是否載入
     * @param loader 類載入器
     * @return 一個Class物件
     * @throws ClassNotFoundException
     */
    @CallerSensitive
    public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException
    {
        Class<?> caller = null;
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
          //只有安全管理員才需要反射呼叫來獲取呼叫者類的存在。否則,避免呼叫來產生額外的開銷
            caller = Reflection.getCallerClass();
            if (sun.misc.VM.isSystemDomainLoader(loader)) {
                ClassLoader ccl = ClassLoader.getClassLoader(caller);
                if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                    sm.checkPermission(
                        SecurityConstants.GET_CLASSLOADER_PERMISSION);
                }
            }
        }
        return forName0(name, initialize, loader, caller);
    }

    其實可以從上文看出java對類的載入策略,載入方式是動態載入的,當需要建立類的例項時,這時候建立一個對應Class類物件,載入進靜態程式碼塊、靜態屬性與靜態方法,然後再載入進對應類的其它方法。也就是jvm在第一次建立使用該類時載入的,將類註冊進jvm,不僅是Class類,ClassLoader類也存在這個方法,Object類、Class類、ClassLoader類是直接與jvm打交道的,它們的物件都由jvm產生與管理:

private static native void registerNatives();
    static {
        registerNatives();
    }

    當呼叫類中靜態的成員變數的引用時或者建立該類例項時(包含靜態成員變數與方法,其中構造方法也屬於靜態成員方法,所以這兩者可以統稱靜態成員變數與方法),jvm會呼叫類載入器載入位元組碼檔案來建立對應的Class類物件,再通過Class類物件裡面類的資訊來建立對應的例項。同時為防止位元組碼檔案被破壞而導致的問題,會加入了對位元組碼檔案的檢測。

    在java中還提供了另外的方式來獲取Class類物件,這種方式就是類名加.class,如下程式碼:

Cat cat=new Cat();
System.out.println(Cat.class==cat.getClass());//true
System.out.println(int.class);//列印int

    要注意的是,非引用型別以及void型別也會建立一個Class類物件。 不僅在Class類裡面提供了類的物件獲取,在ClassLoader類載入器類裡面也存在著Class類物件的獲取,也就是loadClass方法,該方法是一個同步方法,會呼叫父載入器來載入,該方法通過:

private native final Class<?> findLoadedClass0(String name);

    findLoadedClass0本地方法來查詢jvm中指定完全限定名的一個已經載入完的Class類物件:

 private final ClassLoader parent;
 protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,檢測是否已經載入
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //父載入器不為空則呼叫父載入器的loadClass
                        c = parent.loadClass(name, false);
                    } else {
                        //父載入器為空則呼叫Bootstrap Classloader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    //父載入器沒有找到,則呼叫findclass,丟擲未找到異常
                    //該方法就是用來拋異常的
                    c = findClass(name);
                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                //呼叫resolveClass()
                resolveClass(c);
            }
            return c;
        }
    }

    我們可以用以下程式碼來判斷forName方法與loadClass方法有什麼不同:

class Kt{
    static{
        System.out.println("載入了3。。。。");
    }
    {
        System.out.println("載入了。。。。");
    }
    public Kt(){
        System.out.println("載入了2。。。。");
    }

}

public class Test {
    public static void main(String args[]) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        System.out.println("未開啟初始化的forName方法。。。。。。");
        Class.forName("test.Kt",false,Kt.class.getClassLoader());
        System.out.println("開啟初始化的forName方法。。。。。。");
        Class.forName("test.Kt",true,Kt.class.getClassLoader());
        System.out.println("loadClass方法。。。。。。");
        ClassLoader.getSystemClassLoader().loadClass("test.Kt");
    }
}

在這裡插入圖片描述
    從結果來看,loadClass方法與未開啟初始化操作的forName方法一樣只會得到一個含有完全限定名的Class類物件,也就是說loadClass並沒有載入其靜態方法、靜態程式碼塊以及靜態屬性。

    上述講解了怎麼獲得一個Class類物件。下面將要講解怎麼獲得類的其它組成部分,如獲得實現介面的名字、構造體、方法、屬性、包名、父類/介面等等。在java中,每個類的屬性都定義了一個的特殊的類來描述,比如Method類就是用來描述方法的,這些類統一的定義在java.lang.reflect反射包裡,這些都會在講解反射的時候講解到。在討論之前,先來看看java中的AbstractRepository抽象類,這個類是用來存貯資訊的,是一個抽象倉庫類。GenericDeclRepository抽象類是用來存貯通用資訊的,ClassRepository類是用用來存貯類的資訊的,ConstructorRepository類用來存貯構造器的資訊的,而MethodRepository是用來存貯方法資訊的。它們之間的關係如圖示:
在這裡插入圖片描述

    從圖中可以看出,它們之間的繼承關係,以及實現的方法名,除了FiledRepository,其它都繼承GenericDeclRepository倉庫,在GenericDeclRepository裡實現了獲取型別變數的getTypeParameters函式,因為Java預設Filed不需要型別變數的,但是Filed是可以使用泛型的,從其中的getGenericType方法可以看出。而方法倉庫繼承構造器倉庫,從另一方面說明了構造器是特殊的方法。拿獲取類的父類來做例子:

 public Type getGenericSuperclass() {
 		//先得到一個倉庫
        ClassRepository info = getGenericInfo();
        //倉庫未建立,直接呼叫本地方法找父類
        if (info == null) {
            return getSuperclass();
        }
        //判定不是介面
        if (isInterface()) {
            return null;
        }
        //呼叫類倉庫的方法獲取父類
        return info.getSuperclass();
    }

    其實倉庫的建立是起著一種快取的機制,因為如果每次都從jvm中拿類的屬性,會降低java整體的效能,為了提高效能,在第一次從jvm中拿到想要的資訊後存在倉庫中,後面直接從倉庫中獲取。