深刻探究JVM之類加載與雙親委派機制

@java

前言

前面學習了虛擬機的內存結構、對象的分配和建立,但對象所對應的類是怎麼加載到虛擬機中來的呢?加載過程當中須要作些什麼?什麼是雙親委派機制以及爲何要打破雙親委派機制?mysql

類的生命週期

在這裏插入圖片描述
類的生命週期包含了如上的7個階段,其中驗證準備解析統稱爲鏈接 ,類的加載主要是前五個階段,每一個階段基本上保持如上順序開始(僅僅是開始,實際上執行是交叉混合的),只有解析階段不必定,在初始化後也有可能纔開始執行解析,這是爲了支持動態語言。sql

加載

加載就是將字節碼的二進制流轉化爲方法區的運行時數據結構,並生成類所對象的Class對象,字節碼二進制流能夠是咱們編譯後的class文件,也能夠從網絡中獲取,或者運行時動態生成(動態代理)等等。
那何時會觸發類加載呢?這個在虛擬機規範中沒有明肯定義,只是規定了什麼時候須要執行初始化(稍後詳細分析)。數組

驗證

這個階段很好理解,就是進行必要的校驗,確保加載到內存中的字節碼是符合要求的,主要包含如下四個校驗步驟(瞭解便可):tomcat

  • 文件格式校驗:這個階段要校驗的東西很是多,主要的有下面這些(實際上遠遠不止)
    • 是否以魔數0xCAFEBABE開頭。
    • 主、次版本號是否在當前Java虛擬機接受範圍以內。
    • 常量池的常量中是否有不被支持的常量類型(檢查常量tag標誌)。
    • 指向常量的各類索引值中是否有指向不存在的常量或不符合類型的常量。
    • CONSTANT_Utf8_info型的常量中是否有不符合UTF-8編碼的數據。
    • Class文件中各個部分及文件自己是否有被刪除的或附加的其餘信息。
    • 。。。。。。
  • 元數據校驗:對字節碼描述信息進行語義分析。
    • 這個類是否有父類(除了java.lang.Object以外,全部的類都應當有父類)。
    • 這個類的父類是否繼承了不容許被繼承的類(被final修飾的類)。
    • 若是這個類不是抽象類,是否實現了其父類或接口之中要求實現的全部方法。
    • 類中的字段、方法是否與父類產生矛盾(例如覆蓋了父類的final字段,或者出現不符合規則的方法重載,例如方法參數都一致,但返回值類型卻不一樣等)。
    • 。。。。。。
  • 字節碼校驗:確保程序沒有語法和邏輯錯誤,這是整個驗證階段最複雜的一個步驟。
    • 保證任意時刻操做數棧的數據類型與指令代碼序列都能配合工做,例如不會出現相似於「在操做棧放置了一個 int 類型的數據,使用時卻按 long 類型來加載入本地變量表中」這樣的狀況。
    • 保證任何跳轉指令都不會跳轉到方法體之外的字節碼指令上。
    • 保證方法體中的類型轉換老是有效的,例如能夠把-個子類對象賦值給父類數據類型,這是安全的,可是把父類對象賦值給子類數據類型,甚至把對象賦值給與它毫無繼承關係、徹底不相干的一個數據類型,則是危險和不合法的。
    • 。。。。。。
  • 符號引用驗證:這個階段發生在符號引用轉爲直接引用的時候,即其實是在解析階段中進行的。
    • 符號引用中經過字符串描述的全限定名是否能找到對應的類。
    • 在指定類中是否存在符合方法的字段描述符及簡單名稱所描述的方法和字段。
    • 符號引用中的類、字段、方法的可訪問性( private、 protected. public、 )。
    • 是否可被當前類訪問。
    • 。。。。。。

準備

該階段是爲類變量(static)分配內存並設置零值,即類只要通過準備階段其中的靜態變量就是可以使用的了,但此時類變量的值還不是咱們想要的值,須要通過初始化階段纔會將咱們但願的值賦值給對應的靜態變量。安全

解析

解析就是將常量池中的符號引用替換爲直接引用的過程。符號引用就是一個代號,好比咱們的名字,而這裏能夠理解爲就是類的徹底限定名直接引用則是對應的具體的人、物,這裏就是指目標的內存地址。爲何須要符號引用呢?由於類在加載到內存以前尚未分配內存地址,所以必然須要一個東西指代它。這個階段包含了類或接口的解析字段解析類方法解析接口方法解析,在解析的過程當中可能會拋出如下異常:網絡

  • java.lang.NoSuchFieldError:找不到字段
  • java.lang.IllegalAccessError:不具備訪問權限
  • java.lang.NoSuchMethodError:找不到方法

初始化

這是類加載過程當中的最後一個步驟,主要是收集類的靜態變量的賦值動做static塊中的語句合成<cinit>方法,經過該方法根據咱們的意願爲靜態變量賦值以及執行static塊,該方法會被加鎖,確保多線程狀況下只有一個線程能初始化成功,利用該特性能夠實現單例模式。虛擬機規定了有且只有遇到如下狀況時必須先確保對應類的初始化完成(加載、準備必然在此以前):數據結構

  • 遇到new、getstatic、putstatic或invokestatic這四條字節碼指令時。可以生成這四條指令的典型Java代碼場景有:
    • 使用new關鍵字實例化對象的時候。
    • 讀取或設置一個類型的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候。
    • 調用一個類型的靜態方法的時候。
  • 反射調用類時。
  • 當初始化類的時候,若是發現其父類尚未進行過初始化,則須要先觸發其父類的初始化。
  • 當虛擬機啓動時,用戶須要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
  • 當使用JDK 7新加入的動態語言支持時,若是一個java.lang.invoke.MethodHandle實例最後的解析結果爲REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四種類型的方法句柄,而且這個方法句柄對應的類沒有進行過初始化,則須要先觸發其初始化。
  • 當一個接口中定義了JDK 8新加入的默認方法(被default關鍵字修飾的接口方法)時,若是有這個接口的實現類發生了初始化,那該接口要在其以前被初始化。

下面分析幾個案例代碼,讀者們能夠先思考後再運行代碼看看和本身想的是否同樣。多線程

案例一

先定義以下兩個類:app

public class SuperClazz {
	static 	{
		System.out.println("SuperClass init!");
	}
	public static int value=123;
	public static final String HELLOWORLD="hello world";
	public static final int WHAT = value;
}

public class SubClaszz extends SuperClazz {
	static{
		System.out.println("SubClass init!");
	}

}

而後進行下面的調用:

public class Initialization {
	public static void main(String[]args){
		Initialization initialization = new Initialization();
		initialization.M1();
	}
	
	public void M1(){
		System.out.println(SubClaszz.value);
	}
}

第一個案例是經過子類去引用父類中的靜態變量,兩個類都會加載和初始化麼?打印結果看看:

SuperClass init!
123

能夠看到只有父類初始化了,那麼父類必然是加載了的,問題就在於子類有沒有被加載呢?能夠加上參數:-XX:+TraceClassLoading再執行(該參數的做用就是打印被加載了的類),能夠看到子類是被加載了的。因此經過子類引用父類靜態變量,父子類都會被加載,但只有父類會進行初始化
爲何呢?反編譯後能夠看到生成了以下指令:

0: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
3: getstatic     #6                  // Field ex7/init/SubClaszz.value:I
6: invokevirtual #7                  // Method java/io/PrintStream.println:(I)V
9: return

關鍵就是getstatic指令就會觸發類的初始化,可是爲何子類不會初始化呢?由於這個變量是來自於父類的,爲了提升效率,因此虛擬機進行了優化,這種狀況只須要初始化父類就好了。

案例二

調用下面的方法:

public void M2(){
		SubClaszz[]sca = new SubClaszz[10];
	}

執行後能夠發現,使用數組,不會觸發初始化,但父子類都會被加載

案例三

public void M3(){
		System.out.println(SuperClazz.HELLOWORLD);
	}

引用常量不會觸發類的加載和初始化,由於常量在編譯後就已經存在當前class的常量池。

案例四

public void M4(){
		System.out.println(SubClaszz.WHAT);
	}

經過常量去引用其它的靜態變量會發生什麼呢?這個和案例一結果是同樣的。

類加載器

類加載器和雙親委派模型

在咱們平時開發中,肯定一個類須要經過徹底限定名,而不能簡單的經過名字,由於在不一樣的路徑下咱們是能夠定義同名的類的。那麼在虛擬機中又是怎麼區分類的呢?在虛擬機中須要類加載器+徹底限定名一塊兒來指定一個類的惟一性,即相同限定名的類若由兩個不一樣的類加載器加載,那虛擬機就不會把它們當作一個類。從這裏咱們能夠看出類加載器必定是有多個的,那麼不一樣的類加載器是怎麼組織的?它們又分別須要加載哪些類呢?
在這裏插入圖片描述
從虛擬角度看,只有兩種類型的類加載器:啓動類加載器(BootstrapClassLoader)非啓動類加載器。前者是C++實現,屬於虛擬機的一部分,後者則是由Java實現的,獨立於虛擬機的外部,而且所有繼承自抽象類java.lang.ClassLoader。
但從Java自己來看,一直保持着三層類加載器雙親委派的結構,固然除了Java自己提供的三層類加載器,咱們還能夠自定義實現類加載器。如上圖,上面三個就是原生的類加載器,每個都是下一個類加載器的父加載器,注意這裏都是採用組合而非繼承。當開始加載類時,首先交給父加載器加載,父加載器加載了子加載器就不用再加載了,而如果父加載器加載不了,就會交給子加載器加載,這就是雙親委派機制。這就比如工做中遇到了沒法處理的事,你會去請示直接領導,直接領導處理不了,再找上層領導,而後上層領導以爲這是個小事,不用他親自動手,就讓你的直接領導去作,接着他又交給你去作等等。下面來看看每一個類加載器的具體做用:

  • BootstrapClassLoader:啓動類加載器,顧名思義,這個類加載器主要負責加載JDK lib包,以及-Xbootclasspath參數指定的目錄,而且虛擬機對文件名進行了限定,也就是說即便咱們本身寫個jar放入到上述目錄,也不會被加載。因爲該類加載器是C++使用,因此咱們的Java程序中沒法直接引用,調用java.lang.ClassLoader.getClassLoader()方法時默認返回的是null。
  • ExtClassLoader:擴展類加載器,主要負責加載JDK lib/ext包,以及被系統變量java.ext.dirs指向的全部類庫,這個類庫能夠存放咱們本身寫的通用jar。
  • AppClassLoader:應用程序類加載器,負責加載用戶classpath上的全部類。它是java.lang.ClassLoader.getSystemClassLoader()的返回值,也是咱們程序的默認類加載器(若是咱們沒有自定義類加載器的話)。

經過這三個類加載以及雙親委派機制,一個顯而易見的好處就是,不一樣的類隨它的類加載器自然具備了加載優先級,像Object、String等等這些核心類庫天然就會在咱們的應用程序類以前被加載,使得程序更安全,不會出現錯誤,Spring的父子容器也是這樣的一個設計。經過下面這段代碼能夠看到每一個類所對應的類加載器:

public class ClassLoader {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader()); //啓動類加載器
        System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader());//拓展類加載器
        System.out.println(ClassLoader.class.getClassLoader());//應用程序類加載器
    }
}

輸出:

null
sun.misc.Launcher$ExtClassLoader@4b67cf4d
sun.misc.Launcher$AppClassLoader@14dad5dc

破壞雙親委派模型

剛剛我舉了工做中的一個例子來講明雙親委派機制,但現實中咱們不須要事事都去請示領導,一樣類加載器也不是徹底遵循雙親委派機制,在必要的時候是能夠打破這個規則的。下面列舉四個破壞的狀況,在此以前咱們須要先了解下雙親 委派的代碼實現原理,在java.lang.ClassLoader類中有一個loadClass以及findClass方法:

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

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

從上面能夠看到首先是調用parent去加載類,沒有加載到才調用自身的findClass方法去加載。也就是說用戶在實現自定義類加載器的時候須要覆蓋的是fiindClass而不是loadClass,這樣才能知足雙親委派模型
下面具體來看看破壞雙親委派的幾個場景。

第一次

第一次破壞是在雙親委派模型出現以前, 由於該模型是在JDK1.2以後才引入的,那麼在此以前,抽象類java.lang.ClassLoader就已經存在了,用戶自定義的類加載器都會去覆蓋該類中的loadClass方法,因此雙親委派模型出現後,就沒法避免用戶覆蓋該方法,所以新增了findClass引導用戶去覆蓋該方法實現本身的類加載邏輯。

SPI

第二次破壞是因爲這個模型自己缺陷致使的,由於該模型保證了類的加載優先級,可是有些接口是Java定義在覈心類庫中,但具體的服務實現是由用戶提供的,這時候就不得不破壞該模型才能實現,典型的就是Java中的SPI機制(對SPI不瞭解的讀者能夠翻閱我以前的文章或是其它資料,這裏不進行闡述)。J
DBC的驅動加載就是SPI實現的,因此直接看到java.sql.DriverManager類,該類中有一個靜態初始化塊:

static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }

        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

主要看ServiceLoader.load方法,這個就是經過SPI去加載咱們引入java.sql.Driver實現類(好比引入mysql的驅動包就是com.mysql.cj.jdbc.Driver):

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

這個方法主要是從當前線程中獲取類加載器,而後經過這個類加載器去加載驅動實現類(這個叫線程上下文類加載器,咱們也可使用這個技巧去打破雙親委派),那這裏會獲取到哪個類加載器呢?具體的設置是在sun.misc.Launcher類的構造器中:

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " + var2);
            }

            System.setSecurityManager(var3);
        }

    }

能夠看到設置的就是AppClassLoader。你可能會有點疑惑,這個類加載器加載類的時候不也是先調用父類加載器加載麼,怎麼就打破雙親委派了呢?其實打破雙親委派指的就是類的層次結構,延伸意思就是類的加載優先級,這裏本應該是在加載核心類庫的時候卻提早將咱們應用程序中的類庫給加載到虛擬機中來了。

Tomcat

在這裏插入圖片描述
上圖是Tomcat類加載的類圖,前面三個不用說,CommonClassLoaderCatalinaClassLoaderSharedClassLoaderWebAppClassLoaderJspClassLoader則是Tomcat本身實現的類加載器,分別加載common包server包shared包WebApp/WEB-INF/lib包以及JSP文件,前面三個在tomcat 6以後已經合併到根目錄下的lib目錄下。而WebAppClassLoader則是每個應用程序對應一個,JspClassLoader是每個JSP文件都會對應一個,而且這兩個類加載器都沒有父類加載器,這也就違背了雙親委派模型。
爲何每一個應用程序須要單獨的WebAppClassLoader實例?由於每一個應用程序須要彼此隔離,假如在兩個應用中定義了同樣的類(徹底限定名),若是遵循雙親委派那就只會存在一份了,另外不一樣的應用還有可能依賴同一個類庫的不一樣版本,這也須要隔離,因此每個應用程序都會對應一個WebAppClassLoader,它們共享的類庫可讓SharedClassLoader加載,另外這些類加載加載的類對Tomcat自己來講也是隔離的(CatalinaClassLoader加載的)。
爲何每一個JSP文件須要對應單獨的一個JspClassLoader實例?這是因爲JSP是支持運行時修改的,修改後會丟棄掉以前編譯生成的class,並從新生成一個JspClassLoader實例去加載新的class。
以上就是Tomcat爲何要打破雙親委派模型的緣由。

OSGI

OSGI是用於實現模塊熱部署,像Eclipse的插件系統就是利用OSGI實現的,這個技術很是複雜同時使用的也愈來愈少了,感興趣的讀者可自行查閱資料學習,這裏再也不進行闡述。

總結

類加載的過程讓咱們瞭解到一個類是如何被加載到內存中,須要通過哪些階段;而類加載器和雙親委派模型則是告訴咱們應該怎麼去加載類、類的加載優先級是怎樣的,其中的設計思想咱們也能夠學習借鑑;最後須要深入理解的是爲何須要打破雙親委派,在遇到相應的場景時應該怎麼作。

相關文章
相關標籤/搜索