class Class<T> { ... private final ClassLoader classLoader; ... }
JVM 運行實例中會存在多個 ClassLoader,不一樣的 ClassLoader 會從不一樣的地方加載字節碼文件。它能夠從不一樣的文件目錄加載,也能夠從不一樣的 jar 文件中加載,也能夠從網絡上不一樣的靜態文件服務器來下載字節碼再加載。java
JVM 中內置了三個重要的 ClassLoader,分別是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader。mysql
class ClassLoader { ... private final ClassLoader parent; ... }
當咱們在使用 jdbc 驅動時,常常會使用 Class.forName 方法來動態加載驅動類。sql
Class.forName("com.mysql.cj.jdbc.Driver");
其原理是 mysql 驅動的 Driver 類裏有一個靜態代碼塊,它會在 Driver 類被加載的時候執行。這個靜態代碼塊會將 mysql 驅動實例註冊到全局的 jdbc 驅動管理器裏。數組
class Driver { static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException("Can't register driver!"); } } ... }
forName 方法一樣也是使用調用者 Class 對象的 ClassLoader 來加載目標類。不過 forName 還提供了多參數版本,能夠指定使用哪一個 ClassLoader 來加載服務器
Class<?> forName(String name, boolean initialize, ClassLoader cl)
經過這種形式的 forName 方法能夠突破內置加載器的限制,經過使用自定類加載器容許咱們自由加載其它任意來源的類庫。根據 ClassLoader 的傳遞性,目標類庫傳遞引用到的其它類庫也將會使用自定義加載器加載。網絡
ClassLoader 裏面有三個重要的方法 loadClass()、findClass() 和 defineClass()。框架
class ClassLoader { // 加載入口,定義了雙親委派規則 Class loadClass(String name) { // 是否已經加載了 Class t = this.findFromLoaded(name); if(t == null) { // 交給雙親 t = this.parent.loadClass(name) } if(t == null) { // 雙親都不行,只能靠本身了 t = this.findClass(name); } return t; } // 交給子類本身去實現 Class findClass(String name) { throw ClassNotFoundException(); } // 組裝Class對象 Class defineClass(byte[] code, String name) { return buildClassFromCode(code, name); } } class CustomClassLoader extends ClassLoader { Class findClass(String name) { // 尋找字節碼 byte[] code = findCodeFromSomewhere(name); // 組裝Class對象 return this.defineClass(code, name); } }
// ClassLoader 構造器 protected ClassLoader(String name, ClassLoader parent);
雙親委派規則可能會變成三親委派,四親委派,取決於你使用的父加載器是誰,它會一直遞歸委派到根加載器。maven
這兩個方法均可以用來加載目標類,它們之間有一個小小的區別,那就是 Class.forName() 方法能夠獲取原生類型的 Class,而 ClassLoader.loadClass() 則會報錯。工具
Class<?> x = Class.forName("[I"); System.out.println(x); x = ClassLoader.getSystemClassLoader().loadClass("[I"); System.out.println(x); --------------------- class [I Exception in thread "main" java.lang.ClassNotFoundException: [I ...
項目管理上有一個著名的概念叫着「鑽石依賴」,是指軟件依賴致使同一個軟件包的兩個版本須要共存而不能衝突。post
咱們平時使用的 maven 是這樣解決鑽石依賴的,它會從多個衝突的版本中選擇一個來使用,若是不一樣的版本之間兼容性很糟糕,那麼程序將沒法正常編譯運行。Maven 這種形式叫「扁平化」依賴管理。
$ cat ~/source/jcl/v1/Dep.java public class Dep { public void print() { System.out.println("v1"); } } $ cat ~/source/jcl/v2/Dep.java public class Dep { public void print() { System.out.println("v1"); } } $ cat ~/source/jcl/Test.java public class Test { public static void main(String[] args) throws Exception { String v1dir = "file:///Users/qianwp/source/jcl/v1/"; String v2dir = "file:///Users/qianwp/source/jcl/v2/"; URLClassLoader v1 = new URLClassLoader(new URL[]{new URL(v1dir)}); URLClassLoader v2 = new URLClassLoader(new URL[]{new URL(v2dir)}); Class<?> depv1Class = v1.loadClass("Dep"); Object depv1 = depv1Class.getConstructor().newInstance(); depv1Class.getMethod("print").invoke(depv1); Class<?> depv2Class = v2.loadClass("Dep"); Object depv2 = depv2Class.getConstructor().newInstance(); depv2Class.getMethod("print").invoke(depv2); System.out.println(depv1Class.equals(depv2Class)); } }
在運行以前,咱們須要對依賴的類庫進行編譯
$ cd ~/source/jcl/v1 $ javac Dep.java $ cd ~/source/jcl/v2 $ javac Dep.java $ cd ~/source/jcl $ javac Test.java $ java Test v1 v2 false
在這個例子中若是兩個 URLClassLoader 指向的路徑是同樣的,下面這個表達式仍是 false,由於即便是一樣的字節碼用不一樣的 ClassLoader 加載出來的類都不能算同一個類
depv1Class.equals(depv2Class)
咱們還可讓兩個不一樣版本的 Dep 類實現同一個接口,這樣能夠避免使用反射的方式來調用 Dep 類裏面的方法。
Class<?> depv1Class = v1.loadClass("Dep"); IPrint depv1 = (IPrint)depv1Class.getConstructor().newInstance(); depv1.print()
這裏咱們從新理解一下 ClassLoader 的意義,它至關於類的命名空間,起到了類隔離的做用。位於同一個 ClassLoader 裏面的類名是惟一的,不一樣的 ClassLoader 能夠持有同名的類。ClassLoader 是類名稱的容器,是類的沙箱。
class Thread { ... private ClassLoader contextClassLoader; public ClassLoader getContextClassLoader() { return contextClassLoader; } public void setContextClassLoader(ClassLoader cl) { this.contextClassLoader = cl; } ... }
contextClassLoader「線程上下文類加載器」,這到底是什麼東西?
Thread.currentThread().getContextClassLoader().loadClass(name);
這意味着若是你使用 forName(string name) 方法加載目標類,它不會自動使用 contextClassLoader。那些由於代碼上的依賴關係而懶惰加載的類也不會自動使用 contextClassLoader來加載。