在面向對象編程實踐中,咱們經過衆多的類來組織一個複雜的系統,這些類之間相互關聯、調用使他們的關係造成了一個複雜緊密的網絡。當系統啓動時,出於性能、資源利用多方面的考慮,咱們不可能要求 JVM 一次性將所有的類都加載完成,而是隻加載可以支持系統順利啓動和運行的類和資源便可。那麼在系統運行過程當中若是須要使用未在啓動時加載的類或資源時該怎麼辦呢?這就要靠類加載器來完成了。java
類加載器(ClassLoader)就是在系統運行過程當中動態的將字節碼文件加載到 JVM 中的工具,基於這個工具的整套類加載流程,咱們稱做類加載機制。咱們在 IDE 中編寫的都是源代碼文件,之後綴名 .java
的文件形式存在於磁盤上,經過編譯後生成後綴名 .class
的字節碼文件,ClassLoader 加載的就是這些字節碼文件。shell
Java 默認提供了三個 ClassLoader,分別是 AppClassLoader、ExtClassLoader、BootStrapClassLoader,依次後者分別是前者的「父加載器」。父加載器不是「父類」,三者之間沒有繼承關係,只是由於類加載的流程使三者之間造成了父子關係,下文會詳細講述。編程
BootStrapClassLoader 也叫「根加載器」,它是脫離 Java 語言,使用 C/C++ 編寫的類加載器,因此當你嘗試使用 ExtClassLoader 的實例調用 getParent()
方法獲取其父加載器時會獲得一個 null
值。bootstrap
// 返回一個 AppClassLoader 的實例
ClassLoader appClassLoader = this.getClass().getClassLoader();
// 返回一個 ExtClassLoader 的實例
ClassLoader extClassLoader = appClassLoader.getParent();
// 返回 null,由於 BootStrapClassLoader 是 C/C++ 編寫的,沒法在 Java 中得到其實例
ClassLoader bootstrapClassLoader = extClassLoader.getParent();
複製代碼
根加載器會默認加載系統變量 sun.boot.class.path
指定的類庫(jar 文件和 .class 文件),默認是 $JRE_HOME/lib
下的類庫,如 rt.jar、resources.jar 等,具體能夠輸出該環境變量的值來查看。數組
String bootClassPath = System.getProperty("sun.boot.class.path");
String[] paths = bootClassPath.split(":");
for (String path : paths) {
System.out.println(path);
}
// output
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/resources.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/sunrsasign.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jsse.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jce.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/charsets.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/jfr.jar
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/classes
複製代碼
除了加載這些默認的類庫外,也可使用 JVM 參數 -Xbootclasspath/a
來追加額外須要讓根加載器加載的類庫。好比咱們自定義一個 com.ganpengyu.boot.DateUtils
類來讓根加載器加載。緩存
package com.ganpengyu.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtils {
public static void printNow() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()));
}
}
複製代碼
咱們將其製做成一個名爲 gpy-boot
的 jar 包放到 /Users/yu/Desktop/lib
下,而後寫一個測試類去嘗試加載 DateUtils。網絡
public class Test {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("com.ganpengyu.boot.DateUtils");
ClassLoader loader = clz.getClassLoader();
System.out.println(loader == null);
}
}
複製代碼
運行這個測試類:app
java -Xbootclasspath/a:/Users/yu/Desktop/lib/gpy-boot.jar -cp /Users/yu/Desktop/lib/gpy-boot.jar:. Test
複製代碼
能夠看到輸出爲 true
,也就是說加載 com.ganpengyu.boot.DateUtils
的類加載器在 Java 中沒法得到其引用,而任何類都必須經過類加載器加載才能被使用,因此推斷出這個類是被 BootStrapClassLoader 加載的,也證實了 -Xbootclasspath/a
參數確實能夠追加須要被根加載器額外加載的類庫。ide
總之,對於 BootStrapClassLoader 這個根加載器咱們須要知道三點:工具
sun.boot.class.path
指定的類庫-Xbootclasspath/a
參數追加根加載器的默認加載類庫ExtClassLoader 也叫「擴展類加載器」,它是一個使用 Java 實現的類加載器(sun.misc.Launcher.ExtClassLoader
),用於加載系統所須要的擴展類庫。默認加載系統變量 java.ext.dirs
指定位置下的類庫,一般是 $JRE_HOME/lib/ext
目錄下的類庫。
public static void main(String[] args) {
String extClassPath = System.getProperty("java.ext.dirs");
String[] paths = extClassPath.split(":");
for (String path : paths) {
System.out.println(path);
}
}
// output
// /Users/leon/Library/Java/Extensions
// /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/ext
// /Library/Java/Extensions
// /Network/Library/Java/Extensions
// /System/Library/Java/Extensions
// /usr/lib/java
複製代碼
咱們能夠在啓動時修改java.ext.dirs
變量的值來修改擴展類加載器的默認類庫加載目錄,但一般並不建議這樣作。若是咱們真的有須要擴展類加載器在啓動時加載的類庫,能夠將其放置在默認的加載目錄下。總之,對於 ExtClassLoader 這個擴展類加載器咱們須要知道兩點:
java.ext.dirs
參數的值來修改默認加載目錄,若有須要,能夠將要加載的類庫放到這個默認目錄下。AppClassLoader 也叫「應用類加載器」,它和 ExtClassLoader 同樣,也是使用 Java 實現的類加載器(sun.misc.Launcher.AppClassLoader
)。它的做用是加載應用程序 classpath
下全部的類庫。這是咱們最常打交道的類加載器,咱們在程序中調用的不少 getClassLoader()
方法返回的都是它的實例。在咱們自定義類加載器時若是沒有特別指定,那麼咱們自定義的類加載器的默認父加載器也是這個應用類加載器。總之,對於 AppClassLoader 這個應用類加載器咱們須要知道兩點:
classpath
下的類庫。除了上述三種 Java 默認提供的類加載器外,咱們還能夠經過繼承 java.lang.ClassLoader
來自定義一個類加載器。若是在建立自定義類加載器時沒有指定父加載器,那麼默認使用 AppClassLoader 做爲父加載器。關於自定義類加載器的建立和使用,咱們會在後面的章節詳細講解。
上文已經提到過 BootStrapClassLoader 是一個使用 C/C++ 編寫的類加載器,它已經嵌入到了 JVM 的內核之中。當 JVM 啓動時,BootStrapClassLoader 也會隨之啓動並加載核心類庫。當核心類庫加載完成後,BootStrapClassLoader 會建立 ExtClassLoader 和 AppClassLoader 的實例,兩個 Java 實現的類加載器將會加載本身負責路徑下的類庫,這個過程咱們能夠在 sun.misc.Launcher
中窺見。
咱們將 Launcher 類的構造方法源碼精簡展現以下:
public Launcher() {
// 建立 ExtClassLoader
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
// 建立 AppClassLoader
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);
// 建立 SecurityManager
}
複製代碼
能夠看到當 Launcher 被初始化時就會依次建立 ExtClassLoader 和 AppClassLoader。咱們進入 getExtClassLoader()
方法並跟蹤建立流程,發現這裏又調用了 ExtClassLoader 的構造方法,在這個構造方法裏調用了父類的構造方法,這即是 ExtClassLoader 建立的關鍵步驟,注意這裏傳入父類構造器的第二個參數爲 null。接着咱們去查看這個父類構造方法,它位於 java.net.URLClassLoader
類中:
URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory)
複製代碼
經過這個構造方法的簽名和註釋咱們能夠明確的知道,第二個參數 parent
表示的是當前要建立的類加載器的父加載器。結合前面咱們提到的 ExtClassLoader 的父加載器是 JVM 內核中 C/C++ 開發的 BootStrapClassLoader,且沒法在 Java 中得到這個類加載器的引用,同時每一個類加載器又必然有一個父加載器,咱們能夠反證出,ExtClassLoader 的父加載器就是 BootStrapClassLoader。
理清了 ExtClassLoader 的建立過程,咱們來看 AppClassLoader 的建立過程就清晰不少了。跟蹤 getAppClassLoader()
方法的調用過程,能夠看到這個方法自己將 ExtClassLoader 的實例做爲參數傳入,最後仍是調用了 java.net.URLClassLoader
的構造方法,將 ExtClassLoader 的實例做爲父構造器 parent
參數值傳入。因此這裏咱們又能夠肯定,AppClassLoader 的父構造器就是 ExtClassLoader。
將一個 .class
字節碼文件加載到 JVM 中成爲一個 java.lang.Class
實例須要加載這個類的類加載器及其全部的父級加載器共同參與完成,這主要是遵循「雙親委派原則」。
當咱們要加載一個應用程序 classpath
下的自定義類時,AppClassLoader 會首先查看本身是否已經加載過這個類,若是已經加載過則直接返回類的實例,不然將加載任務委託給本身的父加載器 ExtClassLoader。一樣,ExtClassLoader 也會先查看本身是否已經加載過這個類,若是已經加載過則直接返回類的實例,不然將加載任務委託給本身的父加載器 BootStrapClassLoader。
BootStrapClassLoader 收到類加載任務時,會首先檢查本身是否已經加載過這個類,若是已經加載則直接返回類的實例,不然在本身負責的加載路徑下搜索這個類並嘗試加載。若是找到了這個類,則執行加載任務並返回類實例,不然將加載任務交給 ExtClassLoader 去執行。
ExtClassLoader 一樣也在本身負責的加載路徑下搜索這個類並嘗試加載。若是找到了這個類,則執行加載任務並返回類實例,不然將加載任務交給 AppClassLoader 去執行。
因爲本身的父加載器 ExtClassLoader 和 BootStrapClassLoader 都沒能成功加載到這個類,因此最後由 AppClassLoader 來嘗試加載。一樣,AppClassLoader 會在 classpath
下全部的類庫中查找這個類並嘗試加載。若是最後仍是沒有找到這個類,則拋出 ClassNotFoundException
異常。
綜上,當類加載器要加載一個類時,若是本身曾經沒有加載過這個類,則層層向上委託給父加載器嘗試加載。對於 AppClassLoader 而言,它上面有 ExtClassLoader 和 BootStrapClassLoader,因此咱們稱做「雙親委派」。可是若是咱們是使用自定義類加載器來加載類,且這個自定義類加載器的默認父加載器是 AppClassLoader 時,它上面就有三個父加載器,這時再說「雙親」就不太合適了。固然,理解了加載一個類的整個流程,這些名字就無關痛癢了。
「雙親委派機制」最大的好處是避免自定義類和核心類庫衝突。好比咱們大量使用的 java.lang.String
類,若是咱們本身寫的一個 String 類被加載成功,那對於應用系統來講徹底是毀滅性的破壞。咱們能夠嘗試着寫一個自定義的 String 類,將其包也設置爲 java.lang
:
package java.lang;
public class String {
private int n;
public String(int n) {
this.n = n;
}
public String toLowerCase() {
return new String(this.n + 100);
}
}
複製代碼
咱們將其製做成一個 jar 包,命名爲 thief-jdk
,而後寫一個測試類嘗試加載 java.lang.String
並使用接收一個 int 類型參數的構造方法建立實例。
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("java.lang.String");
System.out.println(clz.getClassLoader() == null);
Constructor<?> c = clz.getConstructor(int.class);
String str = (String) c.newInstance(5);
str.toLowerCase();
}
}
複製代碼
運行測試程序
java -cp /Users/yu/Desktop/lib/thief/thief-jdk.jar:. Test
複製代碼
程序拋出 NoSuchMethodException 異常,由於 JVM 不可以加載咱們自定義的 java.lang.String
,而是從 BootStrapClassLoader 的緩存中返回了核心類庫中的 java.lang.String
的實例,且核心類庫中的 String 沒有接收 int 類型參數的構造方法。同時咱們也看到 Class 實例的類加載器是 null
,這也說明了咱們拿到的 java.lang.String
的實例確實是由 BootStrapClassLoader 加載的。
總之,「雙親委派」機制的做用就是確保類的惟一性,最直接的例子就是避免咱們自定義類和核心類庫衝突。
「雙親委派」機制用來保證類的惟一性,那麼 JVM 經過什麼條件來判斷惟一性呢?其實很簡單,只要兩個類的全路徑名稱一致,且都是同一個類加載器加載,那麼就判斷這兩個類是相同的。若是同一份字節碼被不一樣的兩個類加載器加載,那麼它們就不會被 JVM 判斷爲同一個類。
Person 類
public class Person {
private Person p;
public void setPerson(Object obj) {
this.p = (Person) obj;
}
}
複製代碼
setPerson(Object obj)
方法接收一個對象,並將其強制轉換爲 Person 類型賦值給變量 p。
測試類
import java.lang.reflect.Method;
public class Test {
public static void main(String[] args) {
CustomClassLoader classLoader1 = new CustomClassLoader("/Users/yu/Desktop/lib");
CustomClassLoader classLoader2 = new CustomClassLoader("/Users/yu/Desktop/lib");
try {
Class c1 = classLoader1.findClass("Person");
Object instance1 = c1.newInstance();
Class c2 = classLoader2.findClass("Person");
Object instance2 = c2.newInstance();
Method method = c1.getDeclaredMethod("setPerson", Object.class);
method.invoke(instance1, instance2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
CustomClassLoader 是一個自定義的類加載器,它將字節碼文件加載爲字符數組,而後調用 ClassLoader 的 defineClass()
方法建立類的實例,後文會詳細講解怎麼自定義類加載器。在測試類中,咱們建立了兩個類加載器的實例,讓他們分別去加載同一份字節碼文件,即 Person 類的字節碼。而後在實例一上調用 setPerson()
方法將實例二傳入,將實例二強制轉型爲實例一。
運行程序會看到 JVM 拋出了 ClassCastException
異常,異常信息爲 Person cannot be cast to Person
。從這咱們就能夠知道,同一份字節碼文件,若是使用的類加載器不一樣,那麼 JVM 就會判斷他們是不一樣的類型。
「全盤負責」是類加載的另外一個原則。它的意思是若是類 A 是被類加載器 X 加載的,那麼在沒有顯示指定別的類加載器的狀況下,類 A 引用的其餘全部類都由類加載器 X 負責加載,加載過程遵循「雙親委派」原則。咱們編寫兩個類來驗證「全盤負責」原則。
Worker 類
package com.ganpengyu.full;
import com.ganpengyu.boot.DateUtils;
public class Worker {
public Worker() {
}
public void say() {
DateUtils dateUtils = new DateUtils();
System.out.println(dateUtils.getClass().getClassLoader() == null);
dateUtils.printNow();
}
}
複製代碼
DateUtils 類
package com.ganpengyu.boot;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateUtils {
public void printNow() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(new Date()));
}
}
複製代碼
測試類
import com.ganpengyu.full.Worker;
import java.lang.reflect.Constructor;
public class Test {
public static void main(String[] args) throws Exception {
Class<?> clz = Class.forName("com.ganpengyu.full.Worker");
System.out.println(clz.getClassLoader() == null);
Worker worker = (Worker) clz.newInstance();
worker.say();
}
}
複製代碼
運行測試類
java -Xbootclasspath/a:/Users/yu/Desktop/lib/worker.jar Test
複製代碼
運行結果
true
true
2018-09-16 22:34:43
複製代碼
咱們將 Worker 類和 DateUtils 類製做成名爲worker
的 jar 包,將其設置爲由根加載器加載,這樣 Worker 類就必然是被根加載器加載的。而後在 Worker 類的 say()
方法中初始化了 DateUtils 類,而後判斷 DateUtils 類是否由根加載器加載。從運行結果看到,Worker 和其引用的 DateUtils 類都被跟加載器加載,符合類加載的「全盤委託」原則。
「全盤委託」原則實際是爲「雙親委派」原則提供了保證。若是不遵照「全盤委託」原則,那麼同一份字節碼可能會被 JVM 加載出多個不一樣的實例,這就會致使應用系統中對該類引用的混亂,具體能夠參考上文「JVM 怎麼判斷兩個類是相同的」這一節的示例。
除了使用 JVM 預約義的三種類加載器外,Java 還容許咱們自定義類加載器以讓咱們系統的類加載方式更靈活。要自定義類加載器很是簡單,一般只須要三個步驟:
java.lang.ClassLoader
類,讓 JVM 知道這是一個類加載器findClass(String name)
方法,告訴 JVM 在使用這個類加載器時應該按什麼方式去尋找 .class
文件defineClass(String name, byte[] b, int off, int len)
方法,讓 JVM 加載上一步讀取的 .class
文件import java.io.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class CustomClassLoader extends ClassLoader {
private String classpath;
public CustomClassLoader(String classpath) {
this.classpath = classpath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFilePath = getClassFilePath(name);
byte[] classData = readClassFile(classFilePath);
return defineClass(name, classData, 0, classData.length);
}
public String getClassFilePath(String name) {
if (name.lastIndexOf(".") == -1) {
return classpath + "/" + name + ".class";
} else {
name = name.replace(".", "/");
return classpath + "/" + name + ".class";
}
}
public byte[] readClassFile(String filepath) {
Path path = Paths.get(filepath);
if (!Files.exists(path)) {
return null;
}
try {
return Files.readAllBytes(path);
} catch (IOException e) {
throw new RuntimeException("Can not read class file into byte array");
}
}
public static void main(String[] args) {
CustomClassLoader loader = new CustomClassLoader("/Users/leon/Desktop/lib");
try {
Class<?> clz = loader.loadClass("com.ganpengyu.demo.Person");
System.out.println(clz.getClassLoader().toString());
Constructor<?> c = clz.getConstructor(String.class);
Object instance = c.newInstance("Leon");
Method method = clz.getDeclaredMethod("say", null);
method.invoke(instance, null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
示例中咱們經過繼承 java.lang.ClassLoader
建立了一個自定義類加載器,經過構造方法指定這個類加載器的類路徑(classpath)。重寫 findClass(String name)
方法自定義類加載的方式,其中 getClassFilePath(String filepath)
方法和 readClassFile(String filepath)
方法用於找到指定的 .class
文件並加載成一個字符數組。最後調用 defineClass(String name, byte[] b, int off, int len)
方法完成類的加載。
在 main()
方法中咱們測試加載了一個 Person 類,經過 loadClass(String name)
方法加載一個 Person 類。咱們自定義的 findClass(String name)
方法,就是在這裏面調用的,咱們把這個方法精簡展現以下:
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);
} else {
// 全部父加載器都沒法加載,使用根加載器嘗試加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {}
if (c == null) {
// 全部父加載器和根加載器都沒法加載
// 使用自定義的 findClass() 方法查找 .class 文件
c = findClass(name);
}
}
return c;
}
}
複製代碼
能夠看到 loadClass(String name)
方法內部是遵循「雙親委派」機制來完成類的加載。在「雙親」都沒能成功加載類的狀況下才調用咱們自定義的 findClass(String name)
方法查找目標類執行加載。
自定義類加載器的用處有不少,這裏簡單列舉一些常見的場景。
類加載器是 Java 中很是核心的技術,本文僅對類加載器進行了較爲粗淺的分析,若是須要深刻更底層則須要咱們打開 JVM 的源碼進行研讀。「Java 有路勤爲徑,JVM 無涯苦做舟」,與君共勉。