1. 程式人生 > >java學習記錄-類載入機制

java學習記錄-類載入機制

類載入機制主要分3塊:類載入器(ClassLoader)、類載入過程和雙親委託(破壞雙親委託)

 

類載入器:是Java執行時環境(Java Runtime Environment)的一部分,負責動態載入Java類到Java虛擬機器的記憶體空間中。jdk自帶了三種類載入器,分別是引導類載入器(Bootstrap ClassLoader),擴充套件類載入器(Extension ClassLoader),應用程式類載入器(Application ClassLoader)。後兩種載入器是繼承自抽象類java.lang.ClassLoader

 

類載入過程:類載入過程包括載入、驗證、準備、解析和初始化五個階段。

1、載入
簡單的說,類載入階段就是由類載入器負責根據一個類的全限定名來讀取此類的二進位制位元組流到JVM內部,並存儲在執行時記憶體區的方法區,然後將其轉換為一個與目標型別對應的java.lang.Class物件例項(Java虛擬機器規範並沒有明確要求一定要儲存在堆區中,只是hotspot選擇將Class對戲那個儲存在方法區中),這個Class物件在日後就會作為方法區中該類的各種資料的訪問入口。
2、連結
連結階段要做的是將載入到JVM中的二進位制位元組流的類資料資訊合併到JVM的執行時狀態中,經由驗證、準備和解析三個階段。
1)、驗證
驗證類資料資訊是否符合JVM規範,是否是一個有效的位元組碼檔案,驗證內容涵蓋了類資料資訊的格式驗證、語義分析、操作驗證等。
格式驗證:驗證是否符合class檔案規範
語義驗證:檢查一個被標記為final的型別是否包含子類;檢查一個類中的final方法視訊被子類進行重寫;確保父類和子類之間沒有不相容的一些方法宣告(比如方法簽名相同,但方法的返回值不同)
操作驗證:在運算元棧中的資料必須進行正確的操作,對常量池中的各種符號引用執行驗證(通常在解析階段執行,檢查是否通過富豪引用中描述的全限定名定位到指定型別上,以及類成員資訊的訪問修飾符是否允許訪問等)

2)、準備
為類中的所有靜態變數分配記憶體空間,併為其設定一個初始值(由於還沒有產生物件,例項變數不在此操作範圍內)
被final修飾的靜態變數,會直接賦予原值;類欄位的欄位屬性表中存在ConstantValue屬性,則在準備階段,其值就是ConstantValue的值
3)、解析
將常量池中的符號引用轉為直接引用(得到類或者欄位、方法在記憶體中的指標或者偏移量,以便直接呼叫該方法),這個可以在初始化之後再執行。
可以認為是一些靜態繫結的會被解析,動態繫結則只會在執行是進行解析;靜態繫結包括一些final方法(不可以重寫),static方法(只會屬於當前類),構造器(不會被重寫)
3、初始化
將一個類中所有被static關鍵字標識的程式碼統一執行一遍,如果執行的是靜態變數,那麼就會使用使用者指定的值覆蓋之前在準備階段設定的初始值;如果執行的是static程式碼塊,那麼在初始化階段,JVM就會執行static程式碼塊中定義的所有操作。
所有類變數初始化語句和靜態程式碼塊都會在編譯時被前端編譯器放在收集器裡頭,存放到一個特殊的方法中,這個方法就是<clinit>方法,即類/介面初始化方法。該方法的作用就是初始化一箇中的變數,使用使用者指定的值覆蓋之前在準備階段裡設定的初始值。任何invoke之類的位元組碼都無法呼叫<clinit>方法,因為該方法只能在類載入的過程中由JVM呼叫。
如果父類還沒有被初始化,那麼優先對父類初始化,但在<clinit>方法內部不會顯示呼叫父類的<clinit>方法,由JVM負責保證一個類的<clinit>方法執行之前,它的父類<clinit>方法已經被執行。
JVM必須確保一個類在初始化的過程中,如果是多執行緒需要同時初始化它,僅僅只能允許其中一個執行緒對其執行初始化操作,其餘執行緒必須等待,只有在活動執行緒執行完對類的初始化操作之後,才會通知正在等待的其他執行緒。

 

雙親委託(破壞雙親委託):

程式執行後,編譯器把Java檔案編譯成class檔案後,首先負責載入的是系統類載入器,但它不會馬上載入,而是將此任務移送給它的父類載入器擴充套件類載入器載入,擴充套件類載入器也是將此任務移送給引導類載入器載入。

class檔案到了引導類載入器那,它先判斷能不能載入這個類,如果能,就載入;不能,移送給其子載入器,以此類推。最終我們編寫的class都會配置在classpath環境中,所以,這個類載入任務還是由系統類載入器完成。如果系統類載入器都不能載入,就丟擲ClassNotFoundException。

當一個class載入到JVM中,類載入階段已經完成。接下來JVM分配記憶體,對整個class檔案(檔案裡面都是二進位制的彙編命令)進行內容解析(JVM對二進位制的命令逐行解析,交由CPU執行)。

雙親委託機制:這種機制的好處就是不會隨隨便便的載入使用者寫的類。

 

 雙親委託在jdk8中的實現,ClassLoader中的loadClass方法

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) {
                        c = parent.loadClass(name, false);
                    // 前面提到,bootstrap classloader的類載入器為null,通過find方法來獲得
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    // 如果還是沒有獲得該類,呼叫findClass找到類
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // jvm統計
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            // 連線類
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

如果自定義類載入器要破壞雙親委託,重寫loadClass方法即可,反之,重寫findClass方法。這樣保證了不會隨隨便便的載入使用者寫的類

---------------------------------------------------------------------------------------------------------------

附:是否可以寫一個類叫"java.lang.String"

不能寫這個類,我們自定義的類載入器必須繼承自ClassLoader,其loadClass()方法裡呼叫了父類的defineClass()方法,並最終調到preDefineClass()方法,因此我們自定義的類載入器也是不能載入以“java.”開頭的java類的。



 

參考資料:

https://www.cnblogs.com/joemsu/p/9310226.html

https://blog.csdn.net/tang9140/article/details/42738433

https://www.cnblogs.com/xiaoxian1369/p/5498817.html