JVM的藝術—類加載器篇(二)

分享是價值的傳遞,喜歡就點個贊java

引言

今天咱們繼續來深刻的剖析類加載器的內容。上節課咱們講了類加載器的基本內容,沒看過的小夥伴請加關注。今天咱們繼續。mysql

什麼是定義類加載器和初始化類加載器?

  • 定義類加載器:假設咱們的某一個類是由ExtClassLoader加載的,那麼ExtClassLoader稱爲該類的定義類加載器sql

  • 初始化加載器:可以返回Class對象引用的都叫作該類的初始類加載器,好比類A是由咱們的ExtClassLoader加載,那麼數據庫

    ExtClassLoader是該類的定義類加載器,也是該類的初始類加載器,而咱們的AppClassLoader也能返回咱們A類的引用bootstrap

    那麼AppClassLoader也是該類的初始類加載器。api

什麼是類加載器的雙親委派模型?

上篇文章咱們提到了類加載器的雙親委派模型,也能夠稱爲雙親委託模型。今天這篇文章咱們就來把這個概念給講明白。安全

概念:用一種簡單的方式去描述雙親委託的概念。能夠分爲兩個部分去理解app

1委託:

jvm加載類的時候是經過雙親委派的方式去加載,自下而上的去委託。jvm

自定義類加載器須要加載類時,先委託應用類加載器去加載,而後應用類加載器又向擴展類加載器去委託,擴展類加載器在向啓動類加載器去委託。ide

若是啓動類加載器不能加載該類。那麼就向下加載

2加載:

jvm加載類的時候是經過雙親委派的方式去加載委託,可是加載的時候是由上向下去加載的,當委託到最頂層啓動類加載器的時候,沒法在向上委託,那麼

啓動類加載器就開始嘗試去加載這個類,啓動類加載器加載不了就向下交給擴展類加載器去加載,擴展類加載器加載不了就繼續向下委託交給應用類加載器

去加載,以此類推。

若是文字描述你還不清楚什麼是雙親委託機制,那麼我畫了一幅圖能夠更清楚類加載的過程。以下:

經過上圖,咱們知道更能清楚的知道,雙親委託模型的工做機制,用一句簡單的話說,就是須要加載一個類的時候,向上委託,向下加載。

注意:在雙親委派機制中,各個加載器按照父子關係造成樹型結構,除了根加載器之外,每個加載器有且只有一個父加載器。

接下來,我也從jdk底層源碼的角度給你們畫了一張類加載的主要過程,圖以下:

以上就是類加載器加載一個類的重要過程步驟。但願各位小夥兒能夠結合源碼的方式,仔細再研究一下。其實還挺好理解的。

下面我們再說說,java採用雙親委託的方式去加載類,這樣作的好處是什麼呢?

  • 雙親委派模型的好處

    總所周知:java.lang.object類是全部類的父類,因此咱們程序在運行期間會把java.lang.object類加載到內存中,假如java.lang.object類

    可以被咱們自定義類加載器去加載的話,那麼jvm中就會存在多份Object的Class對象,並且這些Class對象是不兼容的。

    因此雙親委派模型能夠保證java核心類庫下的類型的安全。

    藉助雙親委派模型,咱們java核心類庫的類必須是由咱們的啓動類加載器加載的,這樣能夠確保咱們核心類庫只會在jvm中存在一份

    這就不會給自定義類加載器去加載咱們核心類庫的類。

    根據咱們的演示案例,一個class能夠由多個類加載器去加載,同時能夠在jvm內存中存在多個不一樣版本的Class對象,這些對象是不兼容的。

    而且是不能相互轉換的。

什麼是全盤委託加載?

解釋:假如咱們的Person類是由咱們的系統類APP類加載器加載的,而person類所依賴的Dog類也會委託給App系統類進 行加載,這個委託過程也遵循雙親委派模型。代碼以下

  • person類代碼中建立Dog實例

public class Person {

public Person(){
  
      new Dog();
  }

}

public class Dog {

    public Dog(){
        System.out.println("Dog 的構造函數");
    }
}
  • 測試類

    public class MainClass02 {
    
        public static void main(String[] args) throws Exception {
            //建立自定義類加載器的一個實例,而且經過構造器指定名稱
            Test01ClassLoader myClassLoader = new Test01ClassLoader("loader1");
            myClassLoader.setPath("I:\\test\\");
            Class<?> classz = myClassLoader.loadClass("com.test.Person");
            System.out.println(classz.getClassLoader());
            System.out.println(Dog.class.getClassLoader());
        }
    }
    
    
    運行結果:
    
    sun.misc.Launcher$AppClassLoader@18b4aac2
    sun.misc.Launcher$AppClassLoader@18b4aac2
    
    Process finished with exit code 0

    從上面的運行結果,咱們能夠看出,當咱們用自定義類加載器去加載咱們的Person的時候,根據雙親委託模型,咱們的Person並無被自定義類加載(Test01ClassLoader)加載,而是被AppClassloader加載成功,同時根據全盤委託規則,咱們的Dog類也被AppClassLoader加載了。因此你們必定要記住這個相當重要的結論。爲咱們後面的學習打下堅實的基礎。

下面咱們在看一個例子。咱們把類路徑下的Person.class文件刪除掉,而後再運行一下上面的main函數,看看結果。代碼以下:

經過那行結果咱們看出,Person類是由咱們的自定義類加載器加載的。那爲何Dog類沒有進行全盤委託的,這是由於雙親委託模型的緣故,咱們的類路徑下並無Person類,故此AppClassLoader是沒法加載咱們的路徑I:\\test\\下的com.test.Person.class文件的。因此Person類是由咱們自定的類加載器加載的。再看Dog類,因爲它的加載要遵循雙親委託模型,由於類路徑下有Dog.class文件,因此AppClassLoader就能夠加載Dog類。故此加載Dog類的ClassLoader是AppClassLoader。寫到這裏,你們對類加載已經有了一個很是深入的理解。那麼java爲何使用雙親委託模型的好處我相信已經不言而喻了。那麼下面來講說雙親委託模型,有沒有他的弊端呢,或者說有什麼很差的地方嘛?咱們能夠打破這種雙親委託的方式去加載類嘛?下面咱們來看一個例子。

類加載器的命名空間

說到雙親委託模型的弊端,那我就離不開命名空間的概念。

類加載器的命名空間 是由類加載器自己以及全部父加載器所加載出來的binary name(full class name)組成.

①:在同一個命名空間裏,不容許出現二個徹底同樣的binary name。

②:在不一樣的命名空間種,能夠出現二個相同的binary name。當時兩者對應的Class對象是相互不能感知到的,也就是說Class對象的類型是不同的。

解釋:同一個Person.class文件 被咱們的不一樣的類加載器去加載,那麼咱們的jvm內存中會生成二個對應的Person的Class對象,並且這二個對應的Class對象是相互不可見的(經過Class對象反射建立的實例對象相互是不可以兼容的不能相互轉型**

③:子加載器的命名空間中的binary name對應的類中能夠訪問 父加載器命名空間中binary name對應的類,反之不行

下面準備了一張圖,以便於你們的理解。

上面這張圖就很好的解釋了命名空間的概念。你們能夠再好好的體會一下。

咱們光畫圖,光用嘴說並非一種頗有力的證據,就如同我寫在這篇博文的時候所提,咱們在學習和掌握某個概念的時候,就必需要拿出有力的證據,來證實本身的猜測或者是觀點,那咱們就舉一個例子。來驗證一下咱們上面的理論是否正確。代碼以下:

這是Person類的代碼。

package com.test;

public class Person {

    public Person() {
        new Dog();
        System.out.println("Dog的classLoader:-->"+ Dog.class.getClassLoader());
    }

    static{
        System.out.println("person類被初始化了");
    }
}

這是Dog類的代碼。

package com.test;

public class Dog {

    public Dog(){
        System.out.println("Dog 的構造函數");
    }
}

具體的驗證思路是這樣的,首先咱們把Person類的Class文件放到啓動類加載器的加載目錄下(C:\Program Files\Java\jdk1.8.0_144\jre\classes 這是啓動類加載器的加載目錄)來達到Person類交給啓動類加載器加載的目的。

而後呢,咱們讓Dog類去被AppClassLoader(系統類加載器去加載)。而後咱們在Person類中去訪問Dog類。看看可否訪問成功。

測試環境:把咱們的Person.class放置在C:\Program Files\Java\jdk1.8.0_131\jre\classes這個目錄下,那麼咱們的Person.class就會被咱們的啓動類加載器加載,而咱們的Dog類是被AppClassLoader進行加載,咱們的Person類 中引用咱們的Dog類會拋出異常.

建立main方法進行測試:

package com.test;

import java.lang.reflect.Method;

/**
 * jvm 類加載器 第一章
 * @author 奇客時間-時光
 * 自定義類加載器——命名空間
 * 測試父加載所加載的類,不能訪問子加載器所加載的類。
 */
public class MainClass02 {

    public static void main(String[] args) throws Exception {

        System.out.println("Person的類加載器:"+Person.class.getClassLoader());

        System.out.println("Dog的類加載器:"+Dog.class.getClassLoader());

        Class<?> clazz = Person.class;
        clazz.newInstance();


    }
}

運行結果:
    
"C:\Program Files\Java\jdk1.8.0_144\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=59226:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;I:\jvm\out\production\jvm-classloader" com.test.MainClass02
Person的類加載器:null
Dog的類加載器:sun.misc.Launcher$AppClassLoader@18b4aac2
person類被初始化了
Exception in thread "main" java.lang.NoClassDefFoundError: com/test/Dog
	at com.test.Person.<init>(Person.java:7)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at java.lang.Class.newInstance(Class.java:442)
	at com.test.MainClass02.main(MainClass02.java:20)

Process finished with exit code 1

總結:經過上面的代碼咱們就能夠看出來,咱們在Person中去new一個Dog的實例的時候,並無建立成功,而是拋出了Exception in thread "main" java.lang.NoClassDefFoundError: com/test/Dog這樣的異常,這也就證實了,咱們上面所說的結論(父加載器所加載的類,不能訪問子加載所加載的類。)

即啓動類加載器所加載的類,不能訪問系統類加載器所加載的類(AppClassLoader)。

那麼確定會有人問,咱們的子加載器所加載的類,能夠訪問父加載器所加載的類嘛?咱們不妨來證明一下,咱們只須要改動一下MainClass02這個類的代碼便可,讓AppClassLoader去加載Dog類,讓咱們的自定義類加載器去加載咱們的Person類。並在Person類中去訪問Dog類。而後將以前C:\Program Files\Java\jdk1.8.0_131\jre\classes目錄下的Person中的Class文件刪除掉,另外還有把咱們類路徑下的Person文件刪除掉,而且在I:\test\目錄下添加com.test.Person.class文件。代碼以下:

package com.test;

import java.lang.reflect.Method;

/**
 * jvm 類加載器 第一章
 * @author 奇客時間-時光
 * 自定義類加載器
 * 測試子類加載器所加載的類,可否訪問父加載器所加載的類。
 */
public class MainClass02 {

    public static void main(String[] args) throws Exception {
        //建立自定義類加載器的一個實例,而且經過構造器指定名稱
        Test01ClassLoader myClassLoader = new Test01ClassLoader("loader1");
        myClassLoader.setPath("I:\\test\\");
        Class<?> classz = myClassLoader.loadClass("com.test.Person");
        System.out.println(classz.getClassLoader());

        System.out.println("Dog的類加載器:"+Dog.class.getClassLoader());

        classz.newInstance();


    }
}

運行結果:
"C:\Program Files\Java\jdk1.8.0_144\bin\java.exe" "-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\lib\idea_rt.jar=60588:C:\Program Files\JetBrains\IntelliJ IDEA 2019.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_144\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_144\jre\lib\rt.jar;I:\jvm\out\production\jvm-classloader" com.test.MainClass02
本身的類加載器被加載了
com.test.Test01ClassLoader@677327b6
Dog的類加載器:sun.misc.Launcher$AppClassLoader@18b4aac2
Dog 的構造函數

Process finished with exit code 0

從上面的結果能夠看出,Person是由咱們的Test01ClassLoader自定義類加載器所加載的,那麼它的父親加載器是AppClassLoader,顯然Dog類是由咱們的AppClassLoader所加載的。故此代碼正常運行,沒有拋出異常,從而得出結論:

1:父加載器所加載的類,不能訪問子加載器所加載的類。

2:子加載器所加載的類,能夠訪問父加載器所加載的類。

雙親委託模型的弊端

  • 咱們先看一段咱們很是熟悉的數據庫鏈接相關的代碼片斷。

    Class.forName("com.mysql.jdbc.Driver");
    Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/RUNOOB","root","123456");
    Statement stmt = conn.createStatement();

案例分析

  • 在上述圖中的第五步爲何會用線程上下文加載器進行加載呢?
  • 在雙親委託模型的機制下,類的加載是由下而上的。即下層的加載器會委託上層進行加載。有些接口是Java核心庫(rt.jar)提供的例如上面的createStatement接口,而Java核心庫是由啓動類加載器進行加載的。而這些接口的具體實現是來自不一樣的廠商(Mysql)。而具體的實現都是經過依賴jar包放到咱們項目中的classPath下的。Java的啓動類加載器/根類加載器是不會加載這些其餘來源的jar包。
  • 咱們都知道classPath下的jar包是由咱們系統類加載器/應用加載器進行加載,根據咱們雙親委託的機制父類加載器是看不到子類(系統類加載器)所加載的具體實現。createStatement 這個接口是由根類加載器進行加載的 而具體的實現又加載不了。在雙親委託的機制下,createStatement這個接口就無具體的實現。
  • 咱們Java的開發者就經過給當前線程池設置上下文加載器的機制,就能夠由設置的上下文加載器來實現對於接口實現類的加載。換句話說父類加載器可使用當前線程上下文加載器加載父類加載器加載不了的一些接口的實現。完美瞭解決了因爲SPI模型(接口定義在覈心庫中,而實現由各自的廠商以jar的形式依賴到咱們項目中)的接口調用。

下面我提供了一張SPI的流程圖。不知道什麼是SPI的小夥伴兒,能夠看一下這張圖:

從上面的例子,咱們能夠看出,雙親委託模型的弊端。而後咱們的jdk給咱們提供了一種經過修改線程上下文類加載的方式來打破這種雙親委託的規則。關於修改上下文類加載的話題,咱們下個章節再具體的講解。接下來呢,咱們再看看,獲取類加載器的幾個方法。而且奉上翻譯好的java doc文檔。方便咱們後續學習線程類加載器。

獲取類加載器的幾個方法

  • Class.getClassLoader()
/**
* Returns the class loader for the class(返回加載該類的類加載器). Some implementations may use
* null to represent the bootstrap class loader(有一些jvm的實現可能用null來表示咱們的啓動類加載器好比 hotspot).
* This method will return null in such implementations if this class was loaded by the bootstrap class loader.
* 若這個方法返回null的話,那麼這個類是由咱們的啓動類加載器加載
*
* If this object represents a primitive type or void, null is returned.
(原始類型 好比int,long等等的類或者 void類型 那麼他們的類加載器是null)
*
*
*/
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;
}
  • 1:返回表明加載該class的類加載器

  • 2:有一些虛擬機(好比hotspot) 的啓動類加載器是null來表示

  • 3:原始類型 好比int ,long 或者是void類型 ,他們的類加載器是null

  • ClassLoader.getSystemClassLoader()方法解讀

/**
* Returns the system class loader for delegation(該方法返回系統類加載器). This is the default
* delegation parent for new ClassLoader instances(也是咱們本身定義的類加載器的委託父類), and is
* typically the class loader used to start the application(一般系統類加載器是用來啓動咱們的應用的)
*
* This method is first invoked early in the runtime's startup
* sequence(程序在運行早起就會調用該方法), at which point it creates the system class loader and sets it
* as the context class loader of the invoking <tt>Thread</tt>.(在那個時間,調用線程建立咱們的系統類加載器同時把系統類加載器設置到咱們線程上下文中)
*
* <p> The default system class loader is an implementation-dependent
* instance of this class.(這句話沒有很好的理解)
*
* <p> If the system property "<tt>java.system.class.loader</tt>" is defined
* when this method is first invoked then the value of that property is
* taken to be the name of a class that will be returned as the system
* class loader. The class is loaded using the default system class loader
* and must define a public constructor that takes a single parameter of
* type <tt>ClassLoader</tt> which is used as the delegation parent. An
* instance is then created using this constructor with the default system
* class loader as the parameter. The resulting class loader is defined
* to be the system class loader.
咱們能夠經過java.system.class.loader 系統屬性來指定一個自定義的類加載的二進制名稱做爲新的系統類加載器,
在咱們自定的加載中咱們須要定義個帶參數的構造函數,參數爲classLoader,那麼咱們這個自定義的類加載器就會看作系統類加載器

*
* @return The system <tt>ClassLoader</tt> for delegation, or
* <tt>null</tt> if none
*
* @throws SecurityException
* If a security manager exists and its <tt>checkPermission</tt>
* method doesn't allow access to the system class loader.
*
* @throws IllegalStateException
* If invoked recursively during the construction of the class
* loader specified by the "<tt>java.system.class.loader</tt>"
* property.
*
* @throws Error
* If the system property "<tt>java.system.class.loader</tt>"
* is defined but the named class could not be loaded, the
* provider class does not define the required constructor, or an
* exception is thrown by that constructor when it is invoked. The
* underlying cause of the error can be retrieved via the
* {@link Throwable#getCause()} method.
*
* @revised 1.4
*/
@CallerSensitive
public static ClassLoader getSystemClassLoader() {
//初始化系統類加載器
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
  • 1:該方法的做用是返回系統類加載器
  • 2:也是咱們自定義加載器的直接父類
  • 3:系統類加載器是用來啓動咱們的應用的
  • 4:在系統早期,調用線程會建立出咱們的系統類加載器,而且把咱們的系統類加載器設置到當前線程的上下文中.
  • 5:咱們能夠經過系統屬性:java.system.class.loader來指定一個咱們自定義類加載器來充當咱們系統類加載器,不過咱們的咱們自定的加載器須要提供一個帶參數(classloader)的構造器

這篇文章就寫到這裏,jvm的藝術會繼續連載,有興趣的讀者能夠關注我:

JVM的藝術—類加載器篇(一)已完結

JVM的藝術—類加載器篇(二)已完結

JVM的藝術—類加載器篇(三)創做中

筆者公衆號:奇客時間

相關文章
相關標籤/搜索