Java雜談3——類加載機制與初始化順序


  Java語言的哲學:一切都是對象。對於Java虛擬機而言,一個普通的Java類同樣是一個對象,那如果是對象,必然有它的初始化過程。一個類在JVM中被實例化成一個對象,需要經歷三個過程:加載、鏈接和初始化。

JAVA類的加載

  加載:從字節碼二進制文件——.class文件將類加載到內存,從而達到類的從硬盤上到內存上的一個遷移,所有的程序必須加載到內存才能工作。一個Java類在被加載到內存后會在Java堆中創建一個類(java.lang.Class)對象,同時JVM為每個類對象都維護一個常量池(類似於符號表)。

類加載器的分類

  Java類都是由類加載器進行加載,從大的分類來看,Java提供兩種類型的類加載器:和用戶自定義的類加載器。Java默認提供了3個類加載器,分別是:Bootstrap ClassLoader、Extension ClassLoader和App ClassLoader。

  • Bootstrap ClassLoader:這個加載器不是一個Java類,而是由底層的c++實現,負責在虛擬機啟動時加載Jdk核心類庫以及加載后兩個類加載器。
  • Extension ClassLoader:是一個普通的Java類,繼承自ClassLoader類,負責加載{JAVA_HOME}/jre/lib/ext/目錄下的所有jar包。
  • App ClassLoader:是Extension ClassLoader的子對象,負責加載應用程序classpath目錄下的所有jar和class文件。

  除了以上3個類加載其之外,用戶還可以繼承ClassLoader類來自定義相應的類加載器。JVM通過一種雙親委托模型來避免重復加載同一個類,在這種模型中,當一個類C需要被某一個類加載器L加載時,會優先在類加載器L的父類中查找類C是否已經被加載。下面的代碼是具體的雙親委托模式的實現:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        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();
                    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(c);
            }
            return c;
        }
}

所有的Java類加載器都保證是引導類加載器的孩子,具體的ClassLoader體系結構見下圖:

  鏈接:包含了驗證和准備類或者接口、包括了它的直接父類、直接父接口、元素類型以及一些必要的操作。Java虛擬機規范並沒明確要求被准備的類或接口需要被解析,只需要驗證相關的類或接口的字節碼符合JVM規范。

  類的初始化:執行類的static塊和初始化類內部的靜態屬性。

Class.forName()ClassLoader.loadClass() 

  通常用這兩種方式來動態加載一個java類,但是兩個方法之間也是有一些細微的差別。

  Class.forName方式

   查看Class類的具體實現可知,實質上這個方法是調用原生的方法:

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

  形式上類似於Class.forName(name,true,currentLoader);

  綜上所述,Class.forName如果調用成功

  •  保證一個Java類被有效得加載到內存中;
  •  類默認會被初始化,即執行內部的靜態塊代碼以及保證靜態屬性被初始化;
  • 默認會使用當前的類加載器來加載對應的類。

 

  ClassLoader.loadClass方式

  如果采用這種方式的類加載策略,由於雙親托管模型的存在,最終都會將類的加載任務交付給Bootstrap ClassLoader進行加載。跟蹤源代碼,最終會調用原生方法:

private native Class<?> findBootstrapClass(String name);

         與此同時,與上一種方式的最本質的不同是,類不會被初始化。

         總結ClassLoader.loadClass如果調用成功:

  • 類會被加載到內存中;
  •  類不會被初始化,只有在之后被第一次調用時類才會被初始化;
  •  之所以采用這種方式的類加載,是提供一種靈活度,可以根據自身的需求繼承ClassLoader類實現一個自定義的類加載器實現類的加載。(很多開源Web項目中都有這種情況,比如tomcat,struct2,jboss。原因是根據Java Servlet規范的要求,既要Web應用自己的類的優先級要高於Web容器提供的類,但同時又要保證Java的核心類不被任意覆蓋,此時重寫一個類加載器就很必要了)

類初始化順序

         對於普通的Java程序,一般都不需要顯式的聲明來動態加載Java類,只需要用import關鍵字將相關聯的類引入,類被第一次調用的時候,就會被加載初始化。那對於一個類對象,其內部各組成部分的初始化順序又是如何的呢?

         一個Java類對象在初始化的時候必定是按照一定順序初始化其靜態塊、靜態屬性、類內部屬性、構造方法。這里我們討論的初始化分別針對兩個對象,一個是類本身還有一個是類實例化的對象。

         類本身的初始化會在類被加載完畢、鏈接完成之后,由Java虛擬機負責調用<clinit>方法完成。在這個方法中依次完成了堆類內部靜態塊的調用和類內部靜態屬性的初始化(如果存在父類,父類會優先進行初始化)。不論創建多少個實例化的對象,一個類只會被初始化一次。

         類實例化的對象通過new操作創建,Java虛擬機保證一個類在new操作實例化其對象之前已經完成了類的加載、鏈接和初始化。之后Java虛擬機會調用<init>方法完成類實例化對象的初始化。這個方法會優先按照代碼中順序完成對類內部個屬性的初始化,之后再調用類的構造函數(如果有父類,則優先調用父類的構造函數)。

  PS:需要注意的是上述提到的<init><clinit>方法都是非法的Java方法名,是由編譯器命名的,並不能由編碼實現。

  綜上所述,我們大致可以得出以下結論,對於一個類,在實例化一個這個類的對象時,我們可以保證以下這樣的優先級進行初始化:

  類內部靜態塊 > 類靜態屬性 > 類內部屬性 > 類構造函數

 

再討論加載順序

  最近看了幾篇談設計模型單例模式的Java實現的文章,在實現一個具體的線程安全單例Java類中,一個簡單且被推薦的方式使用內部靜態類存儲一個靜態屬性,這就涉及到內部靜態類的初始化順序的問題,結合想到這篇文章中也沒有討論過這個問題,繼而做了一些實驗,代碼如下:

  

public class Test {
    
    public static class Inner{
        
        public final static Test testInstance = new Test(3);
        
        static {
            System.out.println("TestInner Static!");
        }
    }
    
    public static Test getInstance(){
        return Inner.testInstance;
    }
    
    public Test(int i ) {
        System.out.println("Test " + i +" Construct! ");
    }
    
    static {
        System.out.println("Test Stataic");
    }
    
    public static Test testOut = new Test(1);
    
    public static void main(String args[]){
        Test t = new Test(2);
        Test.getInstance();
    }

}

  實驗的結果證明順序如下:

  內部類靜態屬性(或靜態塊)會在內部類第一次被調用的時候按順序被初始化(或執行);而類內部靜態塊的執行先於類內部靜態屬性的初始化,會發生在類被第一次加載初始化的時候;類內部屬性的初始化先於構造函數會發生在一個類的對象被實例化的時候。

  綜合上面的結論,上面這段代碼的結果是什么呢?問題就留給讀者們自行思考吧。

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM