筆者曾經閱讀過周志明的《深刻理解Java虛擬機》這本書,閱讀完後自覺得對jvm有了必定的瞭解,然而當真正碰到問題的時候,才發現本身讀的有多粗糙,也體會到只有實踐才能加深理解,正應對了那句話——「Talk is cheap, show me the code」。前段時間,筆者同事提出了一個關於類加載器破壞雙親委派的問題,以咱們常見到的數據庫驅動Driver爲例,爲何要實現破壞雙親委派,下面一塊兒來重溫一下。java
想要知道爲何要破壞雙親委派,就要先從什麼是雙親委派提及,在此以前,咱們先要了解一些概念:mysql
什麼意思呢?咱們知道,判斷一個類是否相同,一般用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:
,由於二者不屬於同一類加載器加載,因此不能轉化!數組
基於上述的問題:若是不是同一個類加載器加載,即時是相同的class文件,也會出現判斷不想同的狀況,從而引起一些意想不到的狀況,爲了保證相同的class文件,在使用的時候,是相同的對象,jvm設計的時候,採用了雙親委派的方式來加載類。app
雙親委派:若是一個類加載器收到了加載某個類的請求,則該類加載器並不會去加載該類,而是把這個請求委派給父類加載器,每個層次的類加載器都是如此,所以全部的類加載請求最終都會傳送到頂端的啓動類加載器;只有當父類加載器在其搜索範圍內沒法找到所需的類,並將該結果反饋給子類加載器,子類加載器會嘗試去本身加載。jvm
這裏有幾個流程要注意一下:ide
jvm提供了三種系統加載器:ui
附上三者的關係:
雙親委派的實現其實並不複雜,其實就是一個遞歸,咱們一塊兒來看一下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; } }
由於在某些狀況下父類加載器須要委託子類加載器去加載class文件。受到加載範圍的限制,父類加載器沒法加載到須要的文件,以Driver接口爲例,因爲Driver接口定義在jdk當中的,而其實現由各個數據庫的服務商來提供,好比mysql的就寫了MySQL Connector
,那麼問題就來了,DriverManager(也由jdk提供)要加載各個實現了Driver接口的實現類,而後進行管理,可是DriverManager由啓動類加載器加載,只能記載JAVA_HOME的lib下文件,而其實現是由服務商提供的,由系統類加載器加載,這個時候就須要啓動類加載器來委託子類來加載Driver實現,從而破壞了雙親委派,這裏僅僅是舉了破壞雙親委派的其中一個狀況。
咱們結合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; } }
好了,這裏基本就差很少完成整個流程了,一塊兒走一遍:
Driver剩餘的加載過程就省略了,有興趣的園友能夠繼續深刻了解一下,不得不說,jvm博大精深,看起來容易,真正到了用起來才發現各類問題,也只有實踐才能加深理解,最後謝謝各位園友觀看,若是有描述不對的地方歡迎指正,與你們共同進步!
參考部分: