Java類載入器的作用就是在執行時載入類。它通過載入class檔案、網路上的位元組流或其他來源構造Class物件,用於生成物件在程式中執行。
在平時的程式開發中,我們一般不需要操作類載入,因為Java本身的類載入器機制已經幫我們做了很多事情。但後面很多時候,比如說自己開發框架或者排查問題的時候,我們需要理解類載入的機制和如何按自己的需求去自定義類載入器。類載入器知識就像Java開發的一道門,門內外隔離了開發人員對類載入的使用,而瞭解類載入器就掌握了這道門的鑰匙。
什麼是類載入器
類載入器是一個用來載入類檔案的類。Java原始碼通過編譯器編譯後,類載入器載入檔案中的位元組碼來執行程式,位元組碼的來源也可以來自於網路。Java有3中預設的類載入器:Bootstrap類載入器、Extension類載入器和System類載入器(或者叫做Application類載入器)。每個類載入器都設定好從哪裡載入類。
Bootstrap類載入器:JRE/lib/rt.jar中的JDK類檔案
Bootstrap類載入器是所有類載入器的父類(非類繼承關係)。它的大部分由C來寫的,通過程式碼獲取不到,比如呼叫String.class.getClassLoader(),會返回null。
Extension類載入器:JRE/lib/ext或者java.ext.dirs指向的目錄
Extension載入器由sun.misc.Launcher$ExtClassLoader
實現 System類載入器:CLASSPATH環境變數,由-classpath
或-cp
選項定義,或者是JAR中的Manifest的classpath屬性定義。 System類載入器由sun.misc.Launcher$AppClassLoader
實現。
類載入器的工作原理
Java的類載入性保證了很好的穩定性和擴充性。類載入器的工作原理基於三個機制
委託機制
某載入器在嘗試載入類的時候,都會委託其父類載入器嘗試載入類。比如一個應用要載入CLASSPAH下A.class。載入這個類的請求由System類載入器委託父載入器Extension類載入器,Extension類載入器會委託父載入器Bootstrap類載入器。Bootstrap類載入器先從rt.jar中嘗試載入這個類。這個類在rt.jar中不存在,所以載入工作回到Extension類載入器。Extension類載入器會檢視jre/lib/ext目錄下有沒有這個類,如果存在那麼這個類將被載入,且只載入一次。如果沒找到,載入請求有回到了System類載入器。System類載入器從CLASPATH中檢視該類,如果存在則載入這個類,如果沒找到,則報java.lang.ClassNotFoundException。
可見性機制
子類載入器可以看到父類載入器載入的類,而反之則不行。
單一性機制
父載入器載入過的類不能被子載入器載入第二次,同一個類載入器例項也只能載入一個類一次。
如何載入類
我們可以使用顯示呼叫的方式或者交由JVM隱式載入一個類。在類載入的過程中,一般有三個概念上的類載入器提供使用。
CurrentClassLoader,稱之為當前類載入器
SpecificClassLoader, 稱之為指定的類載入器。值得是一個特定的ClassLoader示例
ThreadContextClassLoader,稱之為執行緒上下文類載入器。每個執行緒都會擁有一個ClassLoader引用,而且可以通過Thread.currentThread().setContextClassLoader(ClassLoader classLoader)進行切換
其中CurrentClassLoader的載入過程是JVM執行時候控制的,非顯示呼叫。
顯示載入
Class.forName和ClassLoader.loadClass都可以用來進行類載入。比如
Class.forName("com.xiaoyi.pandora.vo.A");
Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1);
customClassLoader1.loadClass("com.xiaoyi.pandora.vo.A");
複製程式碼
Class.forName的方式其實是使用了CurrentClassLoader這種方式,本質上還是找到一個類載入器去執行ClassLoader.loadClass動作。
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
複製程式碼
從程式碼中可以看出,利用反射的機制獲取執行方法的類示例caller,從而找到載入caller對應類的類載入器。從而將載入的動作交由這個類載入去執行。
類名,類載入器,類示例這三者的關係是緊密相連的。這裡要提到一個資料結構來儲存Java載入類過程中這三者的關係。SystemDictionary(系統字典)。
SystemDictionary以類名和類載入器例項作為一個key,Class物件引用為value。Class物件能過找到它的類載入器,類名和類加器例項對應一個唯一的Class物件。
所以,Class.forName的呼叫方式是先從SystemDictionary獲取當前類載入器,然後以ClassLoader.loadClass的方式去載入一個類。
隱式載入
大多數情況下,程式中的類載入都是通過隱式載入的形式。不需要顯示呼叫ClassLoader物件去載入類。 我們看下面一個很普通的場景。為了顯示實際效果,這裡自定義了一個類載入器去顯示類的載入動作。
public class A {
B b;
public A() {
this.b = new B();
}
public void show() {
System.out.println("b's classLoader is " + b.getClass().getClassLoader());
}
}
複製程式碼
在載入A.class後,如果例項化類A物件,JVM會自動幫我們利用類載入器幫我們載入B類。
String classPath = "/Users/xiaoyi/work";
CustomClassLoader customClassLoader1 = new CustomClassLoader(classPath, "xiao");
Class aClazz = Class.forName("com.xiaoyi.pandora.vo.A", true, customClassLoader1);
Object a = aClazz.newInstance();
複製程式碼
控制檯的輸出如下
xiao classLoader start load class :com.xiaoyi.pandora.vo.A
xiao classLoader start load class :com.xiaoyi.pandora.vo.B
複製程式碼
在不執行Object a = aClazz.newInstance();這條語句的時候,控制檯不會輸出xiao classLoader start load class :com.xiaoyi.pandora.vo.B。
總結:在載入B類時,JVM會隱式獲取載入A類的類加器去執行載入B類的工作。
類載入過程
java.lang.ClassLoader中Class<?> loadClass(String name, boolean resolve)方法介紹了詳細的類載入過程。類載入中重要的四個方法,loadClass是下面1,2,3方法的入口
1、Class<?> findLoadedClass(String name)
判斷類是否已經被載入過。即從SystemDictionary中根據型別和當前類載入器作為key,檢視是否能找到對應的Class
2、Class<?> findClass(String name)
交給子類載入器的擴充
3、void resolveClass(Class<?> c)
將類名,當前類載入器,類示例物件關聯起來,儲存在SystemDictionary中。Java中規範在一個類被使用之前,必須做關聯(Before the class can be used it must be resolved)
4、Class<?> defineClass(String name, byte[] b, int off, int len)
根據類檔案或者網路上的位元組流,轉化為一個Class的例項。常用於自定義類載入器。
java.lang.ClassLoader中Class<?> loadClass(String name, boolean resolve)的程式碼如下(省略了非核心流程的程式碼)
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 {
// 委託Bootstrap類載入器去嘗試載入
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 類載入器本身去載入類
c = findClass(name);
}
}
if (resolve) {
// 關聯類和類載入器的關係
resolveClass(c);
}
return c;
}
}
複製程式碼
一些類載入器的使用場景
自定義類載入器
自定義類載入器是使用類載入器一個基本場景。關鍵的方法在前面“類載入過程”已經給出。主要有2點
1、繼承ClassLoader
2、重寫findClass方法,實現獲取類的位元組流轉化為類例項返回
一個簡單的自定義類載入器如下
public class CustomClassLoader extends ClassLoader {
private String name;
private String classPath;
public CustomClassLoader(String classPath, String name) {
this.name = name;
this.classPath = classPath;
}
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
System.out.println(this.name + " classLoader start load class :" + name);
try {
byte[] classData = getData(name);
if(classData != null) {
return defineClass(name, classData, 0, classData.length);
}
} catch (IOException e) {
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getData(String className) throws IOException {
InputStream in = null;
ByteArrayOutputStream out = null;
String path = classPath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
try {
in = new FileInputStream(path);
out = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int length = 0;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
return out.toByteArray();
} catch (Exception e) {
} finally {
if(in != null) {
in.close();
}
if(out != null) {
out.close();
}
}
return null;
}
}
複製程式碼
應用隔離
從SystemDictionary資料結構中,一個類名和類載入器例項,對應一個Class例項。也就是說一個類檔案,可以交由不同的類載入器去生成不同的類例項,這些類例項是可以在JVM中並存的。但是如果在對類載入器的訪問上做好隔離,這些類例項在JVM中是可以實現隔離的。具體可以參考Tomcat容器如何利用WebappClassLoader實現應用上的隔離的技術相關文件或者Pandora的隔離實現原理。
SPI
SPI全稱是Service Provider Interface, 是JDK內建的一種服務提供發現機制。是一種動態替換髮現機制。舉個例子:有個介面想在執行時才發現具體的實現類,那麼你只需在程式執行前新增一個實現即可。常見的SPI有JDBC、JCE、JNDI、JAXP等。
這些SPI的介面是由Java核心庫來提供,而SPI的實現則是作為第二方jar包被包含進類路徑(classpath)中。以JDBC的mysql為例子,呼叫程式碼如下所示
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/mydb", "root", "root");
複製程式碼
com.mysql.jdbc.Driver中的程式碼如下,在類被載入後,向DriverManager中註冊mysql的Driver。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can\'t register driver!");
}
}
}
複製程式碼
下面來分析一下其中的類載入過程
1、隱式方式載入DriverManager類,因為DriverManager在JDK的rt.jar中,這是對應的類載入器是Bootstrap類載入器
2、DriverManager類載入後,執行靜態程式碼塊,初始化java.sql.Driver的實現類。按照SPI的規範,ServiceLoader會去/META-INF/services下面查詢java.sql.Driver資源
3、ServiceLoader獲取ThreadContextClassLoader,構造ServiceLoader例項,並將ThreadContextClassLoader賦給ServiceLoader例項。這時候的類載入器是System類載入器。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
複製程式碼
4、DriverManager利用ServiceLoader去獲取/META-INF/services下的java.sql.Driver檔案,載入對應的實現類,並初始化具體的Driver類。這時候的類載入器是第3步的System類載入器,它能夠載入classpath下面的類。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
複製程式碼
//driversIterator在呼叫next方法時,載入驅動類
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
複製程式碼
5、Driver類載入例項化後,會執行靜態程式碼將自己註冊到DriverManager中
SPI的實現方式其實是破壞了類載入器的委託機制,在類載入的過程中,Bootstrap類載入在獲取不到具體的SPI Provider實現類的情況下,委託ThreadContextLoader去載入classpath下的實現類。