JVM學習筆記(二)——虛擬機類加載機制

類加載的時機


  • 類加載的生命週期:
  1. 加載
  2. 驗證、準備、解析(統稱爲連接),其中解析過程不像其他過程那樣按部就班,它可以在初始化階段之後再開始,也就是動態分派的基礎,動態綁定。
  3. 初始化
  4. 使用
  5. 卸載
  • 什麼時候開始類加載,共只有五種情況
  1. 遇到new,getstatic,putstatic,invokestatic四條字節碼指令時。從字面意義上我們就很好理解:(1)new 代表最普遍的new一個對象實例的時候。(2)get/put static表示獲取或設置靜態屬性的時候。(3)invoke,調用靜態方法的時候。 這三種情況都會讓類初始化。
  2. 在使用java.lang.reflect,對類進行反射調用的時候,如果類沒有進行初始化,則需要先觸發初始化。舉例:比如JDBC那個,打頭的肯定是Class.forName("....");調用某某數據庫
  3. 當初始化一個類的時候,如果發現父類還沒有進行初始化,需要先初始化父類。這個也很正常,複習一下,類的加載順序:父類靜態域->子類靜態域->父類構造方法->子類構造方法
  4. 虛擬機啓動時,指定的加載主類(含有main方法),會先被初始化
  5. 在JDK 1.7的動態語言支持,如果一個java.lang.invoke.MethodHandle實例最後解析的結果是REF_get/put Static,invokeStatic,句柄所對應的類尚未初始化,則需要先觸發其初始化。

  • 接口與類加載的區別:
  1. 我們知道接口可以多繼承,所以在接口並不要求其父接口全部完成了初始化,只有在真正用到父接口的時候(如引用接口中定義的常量,複習一下,接口中定義的默認是static final,即常量)纔會初始化。

  • 類加載的過程

  • 加載
  1. java類加載的過程相對靈活,可以通過重寫一個類加載器的loadClass()方法自定義。這樣也導致了紛繁不一的技術,例如動態代理技術、JSP生成servlet的技術、Applet的技術、JAR/EAR/WAR包技術
  2. 而針對於數組類又需要特別遵守以下的規則:(1)如果數組類的組件類型(比如String[]類的組件類型就是String,多個String元素構成了這個數組)是引用類型,那麼這個數組將會被在類加載器上的類名稱空間被表示。類的一致性函數依賴於類,類加載器,構成聯合主鍵。即(類,類加載器)->類的一致性。(2)如果組件類型不是引用類型(如int[]),那麼數組會被標記爲引導類加載器關聯。(3)數組類的可見性與它的組件類型一致,如果組件類型不是引用類型,那麼默認爲Public
  3. 加載階段完成後,二進制字節流就存儲在方法區(JDK 1.8移除,改爲元空間,下同)。然後在內存中實例化一個java.lang.Class類對象(對於HotSpot來說,Class對象雖然是對象,但存放在方法區中)。這個對象將作爲程序訪問方法區中的的這些類型數據的外部接口。

  • 驗證
  1. 文件驗證:驗證字節流是否符合Class文件規範
  2. 元數據驗證:對字節碼描述的信息進行語義分析,是否符合java語法規範
  3. 字節碼驗證:最複雜的階段。確定程序語義合法,符合邏輯,不會危害虛擬機。

  • 準備
  1. 正式爲類變量分配內存並設置類變量(static修飾的,實例變量將等待對象實例化的時候在堆中初始化)爲初始值的階段。

  • 解析
  1. 類或者接口的解析(類級解析):(1)如果被解析的類/接口是不是數組類型,那麼把這個解析對象的全限定名傳給相應的類加載器去解析;同時由於元數據驗證等操作,可能會加載父類或實現的接口。(2)如果是數組類型,且數組的元素類型爲對象,那麼回到第一步。(3)解析完成前進行符號引用驗證,確認類加載器對類/接口是否擁有訪問權限。
  2. 字段解析:(1)如果類本身就包含了需要的字段,那麼直接返回直接引用。(2)否則,如果在類中實現了接口,那麼會由下往上遞歸搜索相應的字段。(3)如果類不是Object類,那麼會按照繼承關係由下往上遞歸搜索父類是否匹配。(4)還找不到就拋出異常:NoSuchFieldError。 
    interface  A{
        int i=2;
    }
    interface B extends A{
        int i=3;
    }
    class Const implements A{
    
        public  static int i=1;
    
    }
    
    public class Test extends  Const implements B{
    //    public static int i=1;
    
    
        public static void main(String[] args) {
    
            System.out.println(Test.i);
        }
    
    
    }
    interface  A{
        int i=2;
    }
    interface B extends A{
        int i=3;
    }
    class Const implements A{
    
        public  static int i=1;
    
    }
    
    public class Test extends  Const implements B{
    //    public static int i=1;  
    //注意這個地方,上一行被註釋掉的話,編譯不通過,因爲類本身不含有i,進入(2)/(3)步,這個步驟是沒有太大先後   //次序的,檢測到Test的父類Const和接口B中都有字段i,所以字段i產生了二義性,編譯不通過。
    
        public static void main(String[] args) {
    
            System.out.println(Test.i);
        }
    
    
    }
    

  3. 類方法解析:(1)如果發現解析的是個接口,拋出異常。(2)在自身方法中找,(3)在父類方法中找。(4)在實現的接口及其父接口找,如果找到了說明類是一個抽象類,拋出異常。
  4. 接口方法解析:(1-3相同)只是不再尋找父類(接口哪來的父類啊。。。頂天就繼承一個父接口),但是會一直找到Objcet類

  • 初始化
  1. 初始化階段是執行類構造器<clinit>方法的過程
  2. <clinit>方法是由所有類變量的賦值動作靜態語句塊中的語句合併產生的。
  3. 靜態語句塊中只能訪問到定義在靜態語句塊之前的變量;之後的變量可以賦值,但不能訪問:如下面代碼所示。
    public class Test  {
        static{
            i=0;
            System.out.println(i);//這裏會提示報錯,非法前向引用
        }
        static int i=1;

  4. 接口中不能使用靜態語句塊。但仍可以有變量初始化的賦值操作,因此接口也會生成<clinit>方法。只不過接口不像類那樣先初始化父類,而是父類變量需要初始化纔會初始化。接口的實現類在初始化的時候也不會執行接口的<clinit>
  5. <clinit>在多線程環境下會被虛擬機加上互斥鎖。只有一個線程能拿到這個鎖,其他線程都會被阻塞。但其他線程在被喚醒後並不會再次執行初始化操作,因爲同一個類加載器下,一個類只會被初始化一次。

  • 類加載器 
  1. 判斷一個類是否和另一個類相等(引申到兩個類實例是否相等),由(類加載器,類本身)共同決定



  • 雙親委派模型
  1. 從雙親委派模型來講類加載器有三種(虛擬機角度是兩種,即啓動類和其他):啓動類加載器、擴展類加載器、應用程序類加載器。
  2. 啓動類加載器:HotSpot中的啓動類加載器是用C++實現的。該加載器無法被java程序直接引用,但可以在需要把加載請求委派給引導類時,直接使用null代替,因爲在源碼中就是這麼規定的。。
    /**
         * Returns the class loader for the class.  Some implementations may use
         * null to represent the bootstrap class loader. This method will return
         * null in such implementations if this class was loaded by the bootstrap
         * class loader.
    **/
    public ClassLoader getClassLoader() {
            ClassLoader cl = getClassLoader0();
            if (cl == null)
                return null;
            SecurityManager sm = System.getSecurityManager();
            if (sm != null) {
                ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());	/////
            }
            return cl;
        }
    請注意,在原書中標記的那一行是sm.checkPermission(SecurityConstants.GET_CLASSLOADER_PERMISSION)筆者使用的版本是JDK 1.8 略有出入
  3. 擴展類加載器:可以直接被使用(屬於其他加載器),繼承自抽象類java.lang.ClassLoader()
  4. 應用程序加載器:程序中默認的類加載器
  5. 雙親委派模型要求除了頂層的啓動類加載器以外的其他加載器都應當有自己的父類加載器(爲Null則是指定啓動類加載器爲父加載器),這裏的父子不是繼承關係而是組合關係
  6. 雙親委派模型工作流程:收到類加載請求->委派給父類去完成->僅在父加載器反饋自己無法完成時,由子類嘗試自己完成(啃老)。所以所有的加載請求最終都應該被傳到頂層的啓動類加載器中。
  7. 根據這個特性,Object類應該在所有類加載器中都是一致的,因爲它都會被委派到頂層的啓動類加載器加載。

  • 破壞雙親委派模型
  1. 原因:雙親委派模型並不是一種強制性的要求,而是一種建議的規範。
  2. 第一種破壞可能:因爲雙親委派模型產生的時間是JDK 1.2,並不是java一出來就有的,所以難免在出現之前的代碼邏輯不符合這個要求。以前是重寫ClassLoader中的loadclass方法,在1.2以後提倡重寫findClass方法,由loadClass在父加載器無法加載的情況下,加載findClass方法中的加載器。 
  3. 第二種破壞可能:雙親委派模型自身的缺陷,基礎類(java的API)中需要調用用戶類(比如數據庫,文件資源等)。本來基礎類是應該,也可以被啓動類加載器加載的;但是啓動類加載器是不會能處理用戶類的。這咋整?
  4. 解決方案:採用線程上下文類加載器(Thread Context ClassLoader),這個類加載器可以通過Thread.setContextClassLoader()來設置。如果不設置,首先從父線程中繼承;父線程中也沒有就默認採用系統加載器(應用程序類加載器)加載。
  5. 結果:由JNDI去調用這個類加載器加載需要的資源,當然,這就“本末倒置”了——父加載器請求子加載器去完成加載,也就破壞了雙親委派模型。
  6. 第三種情況:由於“動態性”(代碼的熱交換,模塊熱部署)

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