JVM 雙親委派模型及 SPI 實現原理分析

今天是學習 JVM 的第 n-1 天,在從新整理了類加載機制以後,不可避免地提到了類加載器,和雙親委派模型,本文又從雙親委派模型講解了 SPI 的相關實現,因此之後...java

雙親委派模型

咱們知道類加載機制是將一個類從字節碼文件轉化爲虛擬機能夠直接使用類的過程(不清楚的同窗能夠去看我另一篇文章:「JVM」別再問我什麼是類加載機制),可是是誰來執行這個過程當中的加載過程,它又是如何完成或者說保障了類加載的準確性和安全性呢?答案就是類加載器以及雙親委派機制。mysql

雙親委派模型的工做機制是:當類加載器接收到類加載的請求時,它不會本身去嘗試加載這個類,而是把這個請求委派給父加載器去完成,只有當父類加載器反饋本身沒法完成這個加載請求時,子加載器纔會嘗試本身去加載類。sql

咱們能夠從 JDK 源碼中將它的工做機制一窺究竟。bootstrap

ClassLoader#loadClass(String, boolean)

這是在jdk1.8java.lang.ClassLoader類中的源碼,這個方法就是用於加載指定的類。安全

public class ClassLoader {
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            // 首先,檢查該類是否已經被當前類加載器加載,若當前類加載未加載過該類,調用父類的加載類方法去加載該類(若是父類爲null的話交給啓動類加載器加載)
            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;
        }
    }
}
複製代碼

看完了上面的代碼,咱們知道這就是雙親委派模型代碼層面的解釋:bash

  1. 當類加載器接收到類加載的請求時,首先檢查該類是否已經被當前類加載器加載;
  2. 若該類未被加載過,當前類加載器會將加載請求委託給父類加載器去完成;
  3. 若當前類加載器的父類加載器(或父類的父類……向上遞歸)爲 null,會委託啓動類加載器完成加載;
  4. 若父類加載器沒法完成類的加載,當前類加載器纔會去嘗試加載該類。

類加載器分類

在 JVM 中預約義的類加載器有3種:啓動類加載器(Bootstrap ClassLoader)、擴展類加載器(Extension ClassLoader)、應用類/系統類加載器(App/System ClassLoader),另外還有一種是用戶自定義的類加載器,它們各自有各自的職責。app

啓動類加載器Bootstrap ClassLoader

啓動類加載器做爲全部類加載器的"老祖宗",是由C++實現的,不繼承於java.lang.ClassLoader類。它在虛擬機啓動時會由虛擬機的一段C++代碼進行加載,因此它沒有父類加載器,在加載完成後,它會負責去加載擴展類加載器和應用類加載器。ide

啓動類加載器用於加載 Java 的核心類——位於<JAVA_HOME>\lib中,或者被-Xbootclasspath參數所指定的路徑中,而且是虛擬機可以識別的類庫(僅按照文件名識別,如rt.jar、tools.jar,名字不符合的類庫即便放在lib目錄中也不會被加載)。工具

拓展類加載器Extension ClassLoader

拓展類加載器繼承於java.lang.ClassLoader類,它的父類加載器是啓動類加載器,而啓動類加載器在 Java 中的顯示就是 null。學習

引自 jdk1.8 ClassLoader#getParent() 方法的註釋,這個方法是用於獲取類加載器的父類加載器: Returns the parent class loader for delegation. Some implementations may use null to represent the bootstrap class loader. This method will return null in such implementations if this class loader's parent is the bootstrap class loader.

拓展類加載器負責加載<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變量所指定的路徑的全部類。須要注意的是擴展類加載器僅支持加載被打包爲.jar格式的字節碼文件。

應用類/系統類加載器App/System ClassLoader

應用類加載器繼承於java.lang.ClassLoader類,它的父類加載器是擴展類加載器。

應用類加載器負責加載用戶類路徑classpath上所指定的類庫。

若是應用程序中沒有自定義的類加載器,通常狀況下應用類加載器就是程序中默認的類加載器。

自定義類加載器Custom ClassLoader

自定義類加載器繼承於java.lang.ClassLoader類,它的父類加載器是應用類加載器。

這是普某戶籍自定義的類加載器,可加載指定路徑的字節碼文件。

自定義類加載器須要繼承java.lang.ClassLoader類並重寫findClass方法(下文有說明爲何不重寫loadClass方法)用於實現自定義的加載類邏輯。

雙親委派模型的好處

  1. 基於雙親委派模型規定的這種帶有優先級的層次性關係,虛擬機運行程序時就可以避免類的重複加載。當父類類加載器已經加載過類時,若是再有該類的加載請求傳遞到子類類加載器,子類類加載器執行loadClass方法,而後委託給父類類加載器嘗試加載該類,可是父類類加載器執行Class<?> c = findLoadedClass(name);檢查該類是否已經被加載過這一階段就會檢查到該類已經被加載過,直接返回該類,而不會再次加載此類。
  2. 雙親委派模型可以避免核心類篡改。通常咱們描述的核心類是rt.jar、tools.jar這些由啓動類加載器加載的類,這些類庫在平常開發中被普遍運用,若是被篡改,後果將不堪設想。假設咱們自定義了一個java.lang.Integer類,與好處1同樣的流程,當加載類的請求傳遞到啓動類加載器時,啓動類加載器執行findLoadedClass(String)方法發現java.lang.Integer已經被加載過,而後直接返回該類,加載該類的請求結束。雖然避免核心類被篡改這一點的緣由與避免類的重複加載一致,但這仍是可以做爲雙親委派模型的好處之一的。

雙親委派模型的不足

這裏所說的不足也能夠理解爲打破雙親委派模型,當雙親委派模型不知足用戶需求時,天然是因爲其不足之處,也就促使用戶將其打破。這裏描述的也就是打破雙親委派模型的三種方式。

  1. 因爲歷史緣由(ClassLoader類在 JDK1.0 時就已經存在,而雙親委派模型是在 JDK1.2 以後才引入的),在未引入雙親委派模型時,用戶自定義的類加載器須要繼承java.lang.ClassLoader類並重寫loadClass()方法,由於虛擬機在加載類時會調用ClassLoader#loadClassInternal(String),而這個方法(源碼以下)會調用自定義類加載重寫的loadClass()方法。而在引入雙親委派模型後,ClassLoader#loadClass方法實際就是雙親委派模型的實現,若是重寫了此方法,至關於打破了雙親委派模型。爲了讓用戶自定義的類加載器也聽從雙親委派模型, JDK 新增了findClass方法,用於實現自定義的類加載邏輯。

    class ClassLoader {
        // This method is invoked by the virtual machine to load a class.
        private Class<?> loadClassInternal(String name) throws ClassNotFoundException{
            // For backward compatibility, explicitly lock on 'this' when
            // the current class loader is not parallel capable.
            if (parallelLockMap == null) {
                synchronized (this) {
                     return loadClass(name);
                }
            } else {
                return loadClass(name);
            }
        }
        // 其他方法省略......
    }
    複製代碼
  2. 因爲雙親委派模型規定的層次性關係,致使子類類加載器加載的類能訪問父類類加載器加載的類,而父類類加載器加載的類沒法訪問子類類加載器加載的類。爲了讓上層類加載器加載的類可以訪問下層類加載器加載的類,或者說讓父類類加載器委託子類類加載器完成加載請求,JDK 引入了線程上下文類加載器,藉由它來打破雙親委派模型的屏障。

  3. 當用戶須要程序的動態性,好比代碼熱替換、模塊熱部署等時,雙親委派模型就再也不適用,類加載器會發展爲更爲複雜的網狀結構。

線程上下文類加載器

上面說到雙親委派模型的不足時提到了線程上下文類加載器Thread Context ClassLoader,線程上下文類加載器是定義在Thread類中的一個ClassLoader類型的私有成員變量,它指向了當前線程的類加載器。上文已經提到線程上下文類加載可以讓父類類加載器委託子類類加載器完成加載請求,那麼這是如何實現的呢?下面就來討論一下。

SPI 在 JDBC 中的應用

咱們知道 Java 提供了一些SPI(Service Provider Interface)接口,它容許服務商編寫具體的代碼邏輯來完成該接口的功能。可是 Java 提供的 SPI 接口是在覈心類庫中,由啓動類加載器加載的,廠商實現的具體邏輯代碼是在 classpath 中,是由應用類加載器加載的,而啓動類加載器加載的類沒法訪問應用類加載器加載的類,也就是說啓動類加載器沒法找到 SPI 實現類,單單依靠雙親委派模型就沒法實現 SPI 的功能了,因此線程上下文類加載器應運而生。

在 Java 提供的 SPI 中咱們最經常使用的可能就屬 JDBC 了,下面咱們就以 JDBC 爲例來看一下線程上下文類加載器如何打破雙親委派模型。

回憶一下之前使用 JDBC 的場景,咱們須要建立驅動,而後建立鏈接,就像下面的代碼這樣:

public class ThreadContextClassLoaderDemoOfJdbc {

    public static void main(String[] args) throws Exception {
        // 加載 Driver 的實現類
        Class.forName("com.mysql.jdbc.Driver");
        // 創建鏈接
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mysql", "root", "admin");
    }
}
複製代碼

在 JDK1.6 之後能夠不用寫Class.forName("com.mysql.jdbc.Driver");,代碼依舊能正常運行。這是由於自帶的 jdbc4.0 版本已支持 SPI 服務加載機制,只要服務商的實現類在 classpath 路徑中,Java 程序會主動且自動去加載符合 SPI 規範的具體的驅動實現類,驅動的全限定類名在META-INF.services文件中。

因此,讓咱們把目光聚焦於創建鏈接的語句,這裏調用了DriverManager類的靜態方法getConnection。在調用此方法前,根據類加載機制的初始化時機,調用類的靜態方法會觸發類的初始化,當DriverManager類被初始化時,會執行它的靜態代碼塊。

public class DriverManager {

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

    private static void loadInitialDrivers() {
        String drivers;
        // 省略代碼:首先讀取系統屬性 jdbc.drivers
        
        // 經過 SPI 加載 classpath 中的驅動類
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                // ServiceLoader 類是 SPI 加載的工具類
                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;
            }
        });
        // 省略代碼:使用應用類加載器繼續加載系統屬性 jdbc.drivers 中的驅動類
    }

}
複製代碼

從上面的代碼中能夠看到,程序時經過調用ServiceLoader.load(Driver.class)方法來完成自動加載 classpath 路徑中具體的全部實現了Driver.class接口的廠商實現類,而在ServiceLoader.load()方法中,就是獲取了當前線程上下文類加載器,並將它傳遞下去,將它做爲類加載器去實現加載邏輯的。

public final class ServiceLoader<S> implements Iterable<S>{
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 獲取當前線程的線程上下文類加載器 AppClassLoader,用於加載 classpath 中的具體實現類
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
}
複製代碼

JDK 默認加載當前類的類加載器去加載當前類所依賴且未被加載的類,而ServiceLoader類位於java.util包下,天然是由啓動類加載器完成加載,而廠商實現的具體驅動類是位於 classpath 下,啓動類加載器沒法加載 classpath 目錄的類,而若是加載具體驅動類的類加載器變成了應用類加載器,那麼就能夠完成加載了。

經過跟蹤代碼,不難看出ServiceLoader#load(Class)方法建立了一個LazyIterator類同時返回了一個ServiceLoader對象,前者是一個懶加載的迭代器,同時它也是後者的一個成員變量,當對迭代器進行遍歷時,就觸發了目標接口實現類的加載。

private class LazyIterator implements Iterator<S> {

    public S next() {
        if (acc == null) {
            return nextService();
        } else {
            PrivilegedAction<S> action = new PrivilegedAction<S>() {
                public S run() { return nextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
}
複製代碼

DriverManager#loadInitialDrivers方法,也就是DriverManager類的靜態代碼塊所執行的方法中,有這樣一段代碼:

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;
            }
        });
複製代碼

這段代碼返回了一個ServiceLoader對象,在這個對象中有一個LazyIterator迭代器類,用於存放全部廠商實現的具體驅動類,當咱們對LazyIterator這個迭代器進行遍歷時,就出發了類加載的邏輯。

private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        try {
            // 不用寫 Class.forName("com.mysql.jdbc.Driver"); 的緣由就是在此處會自動調用這個方法
            c = Class.forName(cn, false, loader);
        } catch (ClassNotFoundException x) {
            fail(service, "Provider " + cn + " not found");
        }
        if (!service.isAssignableFrom(c)) {
            fail(service, "Provider " + cn  + " not a subtype");
        }
        try {
            S p = service.cast(c.newInstance());
            providers.put(cn, p);
            return p;
        } catch (Throwable x) {
            fail(service, "Provider " + cn + " could not be instantiated", x);
        }
        throw new Error();          // This cannot happen
    }
複製代碼

每次遍歷都會調用Class.forName(cn, false, loader)方法對指定的類進行加載和實例化操做,這也是前文提到的在 jdk1.6 之後不用在寫Class.forName("com.mysql.jdbc.Driver");的緣由。

在這個方法Class.forName(cn, false, loader)中,傳入的參數 cn 是全路徑類名,false 是指不進行初始化,loader 則是指定完成 cn 類加載的類加載器。

在這裏的 loader 變量,咱們回顧一下前文的描述,在ServiceLoader.load(Driver.class)方法中是否是獲取了線程上下文類加載器並傳遞下去?

不記得?在回過頭去看一遍!

而傳入的線程上下文類加載器會做爲參數傳遞給ServiceLoader類的構造方法

private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
複製代碼

而此處的 cl 變量就是調用DriverManager類靜態方法的線程上下文類加載器,即應用類加載器。

也就是說,經過DriverManager類的靜態方法,實現了由ServiceLoader類觸發加載位於classpath的廠商實現的驅動類。前文已經說過,ServiceLoader類位於java.util包中,是由啓動類加載器加載的,而由啓動類加載器加載的類居然實現了"委派"應用類加載器去加載驅動類,這無疑是與雙親委派機制相悖的。而實現這個功能的,就是線程上下文類加載器。

至此,咱們就分析完線程上下文類加載是如何實現 SPI 的了。

小結

  1. 雙親委派模型的工做機制。
  2. 類加載器的分類及各自的職責。
  3. 雙親委派模型的好處。
  4. 打破雙親委派模型的三種場景。
  5. 線程上下文類加載器在是如何實現 SPI 的。
相關文章
相關標籤/搜索