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

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

JAVA類的加載

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

類加載器的分類

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

  • 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規範。this

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

Class.forName()ClassLoader.loadClass() 

  一般用這兩種方式來動態加載一個java類,可是兩個方法之間也是有一些細微的差異。spa

  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();
    }

}

  實驗的結果證實順序以下:

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

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

相關文章
相關標籤/搜索