【JVM】淺談雙親委派和破壞雙親委派

1、前言

筆者曾經閱讀過周志明的《深刻理解Java虛擬機》這本書,閱讀完後自覺得對jvm有了必定的瞭解,然而當真正碰到問題的時候,才發現本身讀的有多粗糙,也體會到只有實踐才能加深理解,正應對了那句話——「Talk is cheap, show me the code」。前段時間,筆者同事提出了一個關於類加載器破壞雙親委派的問題,以咱們常見到的數據庫驅動Driver爲例,爲何要實現破壞雙親委派,下面一塊兒來重溫一下。java


2、雙親委派

想要知道爲何要破壞雙親委派,就要先從什麼是雙親委派提及,在此以前,咱們先要了解一些概念:mysql

  • 對於任意一個類,都須要由加載它的類加載器和這個類自己來一同確立其在Java虛擬機中的惟一性

什麼意思呢?咱們知道,判斷一個類是否相同,一般用equals()方法,isInstance()方法和isAssignableFrom()方法。來判斷,對於同一個類,若是沒有采用相同的類加載器來加載,在調用的時候,會產生意想不到的結果:sql

public class DifferentClassLoaderTest {

    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
                InputStream stream = getClass().getResourceAsStream(fileName);
                if (stream == null) {
                    return super.loadClass(name);
                }
                try {
                    byte[] b = new byte[stream.available()];
                    // 將流寫入字節數組b中
                    stream.read(b);
                    return defineClass(name, b, 0, b.length);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                return super.loadClass(name);
            }
        };
        Object obj = classLoader.loadClass("jvm.DifferentClassLoaderTest").newInstance();
        System.out.println(obj.getClass());
        System.out.println(obj instanceof DifferentClassLoaderTest);

    }
}

輸出結果:數據庫

class jvm.DifferentClassLoaderTest
false

若是在經過classLoader實例化的使用,直接轉化成DifferentClassLoaderTest對象:bootstrap

DifferentClassLoaderTest obj = (DifferentClassLoaderTest) classLoader.loadClass("jvm.DifferentClassLoaderTest").newInstance();

就會直接報java.lang.ClassCastException:,由於二者不屬於同一類加載器加載,因此不能轉化!數組


2.一、爲何須要雙親委派

基於上述的問題:若是不是同一個類加載器加載,即時是相同的class文件,也會出現判斷不想同的狀況,從而引起一些意想不到的狀況,爲了保證相同的class文件,在使用的時候,是相同的對象,jvm設計的時候,採用了雙親委派的方式來加載類。app

雙親委派:若是一個類加載器收到了加載某個類的請求,則該類加載器並不會去加載該類,而是把這個請求委派給父類加載器,每個層次的類加載器都是如此,所以全部的類加載請求最終都會傳送到頂端的啓動類加載器;只有當父類加載器在其搜索範圍內沒法找到所需的類,並將該結果反饋給子類加載器,子類加載器會嘗試去本身加載。jvm

這裏有幾個流程要注意一下:ide

  1. 子類先委託父類加載
  2. 父類加載器有本身的加載範圍,範圍內沒有找到,則不加載,並返回給子類
  3. 子類在收到父類沒法加載的時候,纔會本身去加載

jvm提供了三種系統加載器:ui

  1. 啓動類加載器(Bootstrap ClassLoader):C++實現,在java裏沒法獲取,負責加載 /lib 下的類。
  2. 擴展類加載器(Extension ClassLoader): Java實現,能夠在java裏獲取,負責加載 /lib/ext 下的類。
  3. 系統類加載器/應用程序類加載器(Application ClassLoader):是與咱們接觸對多的類加載器,咱們寫的代碼默認就是由它來加載,ClassLoader.getSystemClassLoader返回的就是它。

附上三者的關係:

雙親委派圖


2.二、雙親委派的實現

雙親委派的實現其實並不複雜,其實就是一個遞歸,咱們一塊兒來看一下ClassLoader裏的代碼:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
    {
        // 同步上鎖
        synchronized (getClassLoadingLock(name)) {
            // 先查看這個類是否是已經加載過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 遞歸,雙親委派的實現,先獲取父類加載器,不爲空則交給父類加載器
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    // 前面提到,bootstrap classloader的類加載器爲null,經過find方法來得到
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    // 若是仍是沒有得到該類,調用findClass找到類
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // jvm統計
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            // 鏈接類
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }


3、破壞雙親委派

3.一、爲何須要破壞雙親委派?

由於在某些狀況下父類加載器須要委託子類加載器去加載class文件。受到加載範圍的限制,父類加載器沒法加載到須要的文件,以Driver接口爲例,因爲Driver接口定義在jdk當中的,而其實現由各個數據庫的服務商來提供,好比mysql的就寫了MySQL Connector,那麼問題就來了,DriverManager(也由jdk提供)要加載各個實現了Driver接口的實現類,而後進行管理,可是DriverManager由啓動類加載器加載,只能記載JAVA_HOME的lib下文件,而其實現是由服務商提供的,由系統類加載器加載,這個時候就須要啓動類加載器來委託子類來加載Driver實現,從而破壞了雙親委派,這裏僅僅是舉了破壞雙親委派的其中一個狀況。

3.二、破壞雙親委派的實現

咱們結合Driver來看一下在spi(Service Provider Inteface)中如何實現破壞雙親委派。

先從DriverManager開始看,平時咱們經過DriverManager來獲取數據庫的Connection:

String url = "jdbc:mysql://localhost:3306/testdb";
Connection conn = java.sql.DriverManager.getConnection(url, "root", "root");

在調用DriverManager的時候,會先初始化類,調用其中的靜態塊:

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

private static void loadInitialDrivers() {
    ...
        // 加載Driver的實現類
        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) {
                }
                return null;
            }
        });
    ...
}

爲了節約空間,筆者省略了一部分的代碼,重點來看一下ServiceLoader.load(Driver.class)

public static <S> ServiceLoader<S> load(Class<S> service) {
    // 獲取當前線程中的上下文類加載器
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

能夠看到,load方法調用獲取了當前線程中的上下文類加載器,那麼上下文類加載器放的是什麼加載器呢?

public Launcher() {
    ...
    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);
    ...
}

sun.misc.Launcher中,咱們找到了答案,在Launcher初始化的時候,會獲取AppClassLoader,而後將其設置爲上下文類加載器,而這個AppClassLoader,就是以前上文提到的系統類加載器Application ClassLoader,因此上下文類加載器默認狀況下就是系統加載器

繼續來看下ServiceLoader.load(service, cl)

public static <S> ServiceLoader<S> load(Class<S> service,
                                        ClassLoader loader){
    return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // ClassLoader.getSystemClassLoader()返回的也是系統類加載器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

上面這段就不解釋了,比較簡單,而後就是看LazyIterator迭代器:

private class LazyIterator implements Iterator<S>{
    // ServiceLoader的iterator()方法最後調用的是這個迭代器裏的next
    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);
        }
    }
    
    private S nextService() {
        if (!hasNextService())
            throw new NoSuchElementException();
        String cn = nextName;
        nextName = null;
        Class<?> c = null;
        // 根據名字來加載類
        try {
            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
    }
    
    public boolean hasNext() {
        if (acc == null) {
            return hasNextService();
        } else {
            PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
                public Boolean run() { return hasNextService(); }
            };
            return AccessController.doPrivileged(action, acc);
        }
    }
    
    
    private boolean hasNextService() {
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            try {
                // 在classpath下查找META-INF/services/java.sql.Driver名字的文件夾
                // private static final String PREFIX = "META-INF/services/";
                String fullName = PREFIX + service.getName();
                if (loader == null)
                    configs = ClassLoader.getSystemResources(fullName);
                else
                    configs = loader.getResources(fullName);
            } catch (IOException x) {
                fail(service, "Error locating configuration files", x);
            }
        }
        while ((pending == null) || !pending.hasNext()) {
            if (!configs.hasMoreElements()) {
                return false;
            }
            pending = parse(service, configs.nextElement());
        }
        nextName = pending.next();
        return true;
    }

}

好了,這裏基本就差很少完成整個流程了,一塊兒走一遍:

spi加載過程


4、總結

Driver剩餘的加載過程就省略了,有興趣的園友能夠繼續深刻了解一下,不得不說,jvm博大精深,看起來容易,真正到了用起來才發現各類問題,也只有實踐才能加深理解,最後謝謝各位園友觀看,若是有描述不對的地方歡迎指正,與你們共同進步!




參考部分:

相關文章
相關標籤/搜索