天天看點

JVM:類加載機制(類加載方式)

一、目标

        了解什麼是類加載器,以及類加載的方式。

二、前言

        在得到了 Java 程式的位元組代碼之後,需要通過一種方式把位元組代碼加載到虛拟機中運作。Java 選擇了一種更加靈活和開放的方式來實作這個加載過程,虛拟機設計團隊把類加載階段中“通過一個類的全限定名來描述此類的二進制位元組流”這個動作放到 JVM 外部來實作,這個實作即是類加載器(Class Loader)。

三、類加載器

1、概述

        Java 中的類加載器并未以特殊的方式實作的,類加載器本身也是一個 Java 類,在 Java 标準庫中的 java.lang.ClassLoader 類就是其他由 Java 代碼建立的類加載器的父類,其根本的目的隻有一個,即從包含位元組代碼的位元組流中定義出虛拟機中的 Class 對象,一旦得到 Class 類的對象之後,一個 Java 類就可以在虛拟機中自由使用,包括建立新的對象或調用類中的靜态方法等。

        由于類加載器本身也是 Java 類,是以類加載器自身也需要另外的類加載器來加載。

        注意,類加載器隻有自身被加載到虛拟機中之後才能加載其他的 Java 類,這似乎是一個無法解決的循環問題呀。

        實際上,Java 平台提供了一個由原生代碼來實作的啟動類加載器(Bootstrap Class Loader) 負責加載 Java 自身的核心類到虛拟機中,在啟動類加載完成初始的加載工作之後,其他繼承其 ClassLoader 類的類加載就可以正常工作。

        除此之外,還有一類使用者自定義的類加載器。兩者的差別在于啟動類加載器是由原生代碼實作的,而使用者自定義的類加載繼承自 ClassLoader 類。

        在使用者自定義的類加載器中,一部分類加載由 Java 平台預設提供,另一部分由程式自己建立。預設提供的使用者自定義類加載器有兩個:擴充類加載器 和 系統類加載器。

        當虛拟機啟動時,會形成由三個類加載器組成的初始類加載器層次結構:

JVM:類加載機制(類加載方式)

        為什麼要有三個類加載器,一方面是分工,各自負責各自的區塊,另一方面為了實作委托模型,三種類型的類加載分别是:

  • 啟動(Bootstrap)類加載器:啟動類加載器是用本地代碼實作的類加載器,它負責将JAVA_HOME/lib下面的核心類庫或-Xbootclasspath選項指定的jar包等虛拟機識别的類庫加載到記憶體中。由于啟動類加載器涉及到虛拟機本地實作細節,開發者無法直接擷取到啟動類加載器的引用。具體可由啟動類加載器加載到的路徑可通過 System.getProperty(“sun.boot.class.path”) 檢視。
  • 擴充(Extension)類加載器:擴充類加載器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)實作的,它負責将JAVA_HOME /lib/ext或者由系統變量-Djava.ext.dir指定位置中的類庫加載到記憶體中。開發者可以直接使用标準擴充類加載器,具體可由擴充類加載器加載到的路徑可通過 System.getProperty(“java.ext.dirs”) 檢視。
  • 系統(System)類加載器:系統類加載器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)實作的,它負責将使用者類路徑(java -classpath或-Djava.class.path變量所指的目錄,即目前類所在路徑及其引用的第三方類庫的路徑)下的類庫加載到記憶體中。開發者可以直接使用系統類加載器,具體可由系統類加載器加載到的路徑可通過 System.getProperty(“java.class.path”) 檢視。
2、如何協調工作

        如前面所示,Java 中有三種類加載器,那問題來了,碰到一個類需要加載時,它們之間是如何協調工作的,即 Java 是如何區分一個類該由哪個類加載器來完成呢?

        在這裡 Java 采用了 委托模型機制,簡單來說,就是類加載器有載入類的需求時,會先請示其 Parent 使用其搜尋路徑幫忙載入,如果 Parent 找不到,那麼才由自己依照自己的搜尋路徑搜尋類。

JVM:類加載機制(類加載方式)
3、源碼分析

        通過分析标準擴充類加載器和系統類加載器的代碼以及其公共父類(java.net.URLClassLoader和java.security.SecureClassLoader)的代碼可以看出,都沒有覆寫java.lang.ClassLoader中預設的加載委派規則 — loadClass(…)方法。

        既然這樣,我們就可以從java.lang.ClassLoader中的loadClass(String name)方法的代碼中分析出虛拟機預設采用的雙親委派機制到底是什麼模樣:

java.lang.ClassLoader代碼片段

public Class<?> loadClass(String name) throws ClassNotFoundException {  
    return loadClass(name, false);  
}  
  
protected synchronized Class<?> loadClass(String name, boolean resolve)  
        throws ClassNotFoundException {  
  
    // 首先判斷該類型是否已經被加載  
    Class c = findLoadedClass(name);  
    if (c == null) {  
        //如果沒有被加載,就委托給父類加載或者委派給啟動類加載器加載  
        try {  
            if (parent != null) {  
                //如果存在父類加載器,就委派給父類加載器加載  
                c = parent.loadClass(name, false);  
            } else {    // 遞歸終止條件
                // 由于啟動類加載器無法被Java程式直接引用,是以預設用 null 替代
                // parent == null就意味着由啟動類加載器嘗試加載該類,  
                // 即通過調用 native方法 findBootstrapClass0(String name)加載  
                c = findBootstrapClass0(name);  
            }  
        } catch (ClassNotFoundException e) {  
            // 如果父類加載器不能完成加載請求時,再調用自身的findClass方法進行類加載,若加載成功,findClass方法傳回的是defineClass方法的傳回值
            // 注意,若自身也加載不了,會産生ClassNotFoundException異常并向上抛出
            c = findClass(name);  
        }  
    }  
    if (resolve) {  
        resolveClass(c);  
    }  
    return c;  
} 
           

        通過上面的代碼分析,我們可以對JVM采用的雙親委派類加載機制有了更直接的認識。

JVM:類加載機制(類加載方式)

        舉一個例子來說明,為了更好的了解,先弄清楚幾行代碼:

Public class Test{
	Public static void main(String[] arg){
		ClassLoader c  = Test.class.getClassLoader();  //擷取Test類的類加載器
		System.out.println(c); 
		ClassLoader c1 = c.getParent();  //擷取c這個類加載器的父類加載器
		System.out.println(c1);
		ClassLoader c2 = c1.getParent();//擷取c1這個類加載器的父類加載器
		System.out.println(c2);
	}
}/** Output:
。。。AppClassLoader。。。

。。。ExtClassLoader。。。

Null
*/
           

        可以看出 Test 是由 AppClassLoader加載器加載的,AppClassLoader的 Parent 加載器是 ExtClassLoader,但是ExtClassLoader的Parent為 null 是怎麼回事呵,朋友們留意的話,前面有提到Bootstrap Loader是用原生代碼(C++語言)寫的,依 Java 的觀點來看,邏輯上并不存在Bootstrap Loader的類實體,是以在 Java 程式代碼裡試圖列印出其内容時,我們就會看到輸出為null。

        我們還是借助于代碼分析一下,首先看一下java.lang.ClassLoader抽象類中預設實作的兩個構造函數:

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}

private ClassLoader(Void unused, ClassLoader parent) {
	//預設将父類加載器設定為系統類加載器,getSystemClassLoader()擷取系統類加載器 
    this.parent = parent;
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        domains =
            Collections.synchronizedSet(new HashSet<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}
           

        緊接着,我們再看一下ClassLoader抽象類中parent成員的聲明:

// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
           

        聲明為私有變量的同時并沒有對外提供可供派生類通路的public或者protected設定器接口(對應的setter方法)。

        事實上,這就是啟動類加載器、标準擴充類加載器和系統類加載器之間的委派關系。

引用

《深入了解Java7:核心技術與最佳實踐》

https://www.hollischuang.com/archives/199

https://blog.csdn.net/justloveyou_/article/details/72217806