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

雕爺的架構之路發表於2020-10-31

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

引言

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

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

  • 定義類載入器:假設我們的某一個類是由ExtClassLoader載入的,那麼ExtClassLoader稱為該類的定義類載入器

  • 初始化載入器:能夠返回Class物件引用的都叫做該類的初始類載入器,比如類A是由我們的ExtClassLoader載入,那麼

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

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

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

上篇文章我們提到了類載入器的雙親委派模型,也可以稱為雙親委託模型。今天這篇文章我們就來把這個概念給講明白。

概念:用一種簡單的方式去描述雙親委託的概念。可以分為兩個部分去理解

1委託:

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

自定義類載入器需要載入類時,先委託應用類載入器去載入,然後應用類載入器又向擴充套件類載入器去委託,擴充套件類載入器在向啟動類載入器去委託。

如果啟動類載入器不能載入該類。那麼就向下載入

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的藝術—類載入器篇(三)創作中

筆者公眾號:奇客時間

相關文章