在上一篇文章中,我們梳理了 類加載器的基本概念 :類的生命周期、類加載器的作用、類的加載和解除安裝的時機等等,這篇文章我們接着前文繼續複習類加載器的知識,主要包括:JVM中有哪些類加載器?它們之間是什麼關系?什麼是雙親委派機制?
雙親委派模型
四種類加載器
從JVM的角度看,類加載器主要有兩類:Bootstrap ClassLoader和其他類加載,Bootstrap ClassLoader是C++語言實作,是虛拟機自身的一部分;其他類加載器都是Java語言實作,不屬于虛拟機,全部繼承自抽象類java.lang.ClassLoader。
從Java開發者的角度看,需要了解類加載器的雙親委派模型,如下圖所示:
- Bootstrap ClassLoader:啟動類加載器,這個類加載器将負責存放在/lib目錄中、被-Xbootclasspath參數所指定的路徑中,并且是虛拟機會識别的jar類庫加載到記憶體中。更直白點說,就是我們常用的java.lang開頭的那些類,一定是被Bootstrap ClassLoader加載的。
- Extension ClassLoader:擴充類加載器,這個類加載器由sun.misc.Launcher$ExtClassLoader實作,它負責加載/lib/ext目錄中的、或者被java.ext.dirs系統變量指定的路徑中的所有類庫。
- Application ClassLoader:應用程式類加載器,這個類加載器由sun.misc.Launcher$AppClassLoader實作,它負責加載使用者CLASSPATH環境變量指定的路徑中的所有類庫。如果應用程式中沒有自定義過自己的類加載器,這個就是一個Java程式中預設的類加載器。
- 使用者自定義的類加載器:使用者在需要的情況下,可以實作自己的自定義類加載器,一般而言,在以下幾種情況下需要自定義類加載器:(1)隔離加載類。某些架構為了實作中間件和應用程式的子產品的隔離,就需要中間件和應用程式使用不同的類加載器;(2)修改類加載的方式。類加載的雙親委派模型并不是強制的,使用者可以根據需要在某個時間點動态加載類;(3)擴充類加載源,例如從資料庫、網絡進行類加載;(4)防止源代碼洩露。Java代碼很容易被反編譯和篡改,為了防止源碼洩露,可以對類的位元組碼檔案進行加密,并編寫自定義的類加載器來加載自己的應用程式的類。
例子1:不同的類加載器
在下面的代碼中,java.util.HashMap是rt.jar包中的類,是以它的類加載器是null,DNSNameService類是放在ext目錄下的jar包中的類,是以它的類加載器是ExtClassLoader;MyClassLoaderTest的類加載器就是應用類加載器。
import java.util.HashMap;
import sun.net.spi.nameservice.dns.DNSNameService;
public class MyClassLoaderTest {
public static void main(String[] args) {
System.out.println("class loader for HashMap: " + HashMap.class.getClassLoader());
System.out.println(
"class loader for DNSNameService: " + DNSNameService.class.getClassLoader());
System.out.println("class loader for this class: " + MyClassLoaderTest.class.getClassLoader());
System.out.println("class loader for Blob class: " + com.mysql.jdbc.Blob.class.getClassLoader());
}
}
運作上述代碼的接入過下圖所示:
例子2:不同類加載器管理的檔案路徑
通過下面的這個程式,可以看到,每個類加載器負責的jar檔案路徑都不一樣:
public class JVMClassLoader {
public static void main(String[] args) {
System.out.println("引導類加載器加載路徑:" + System.getProperty("sun.boot.class.path"));
System.out.println("擴充類加載器加載路徑:" + System.getProperty("java.ext.dirs"));
System.out.println("系統類加載器加載路徑:" + System.getProperty("java.class.path"));
}
}
例子3:Arthas中的classloader指令
Arthas中提供了classloader指令,可以用來檢視目前應用中的類加載器相關的統計資訊,如下圖所示,
- 輸入classloader後展示的表格彙總了目前應用的類加載器、每個類加載器的執行個體個數、每個類加載器加載的類的個數。
- 輸入classloader -t後,展示了目前應用中類加載器的層次結構,可以看出,BootStrap ClassLoader确實在類加載器體系的頂層,接下來是擴充類加載器,再然後是應用類加載器,這裡還有一個ArthasClassLoader,是Arthas自己實作的一個自定義類加載器。
雙親委派模型的工作過程
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是如此,是以所有的加載請求最終都應該傳送到頂層的啟動類加載器中,隻有當父加載器回報自己無法完成這個加載請求(它的搜尋範圍中沒有找到所需的類)時,子加載器才會嘗試自己去加載。
使用雙親委派模型來組織類加載器之間的關系,有一個顯而易見的好處就是Java類随着它的類加載器一起具備了一種帶有優先級的層次關系。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,是以Object類在程式的各種類加載器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果使用者自己編寫了一個稱為java.lang.Object的類,并放在程式的Class Path中,那系統中将會出現多個不同的Object類,Java類型體系中最基礎的行為也就無法保證,應用程式也将會變得一片混亂。
雙親委派模型的實作非常簡單,實作雙親委派的代碼在java.lang.ClassLoader的loadClass()方法之中,如下面的代碼所示:
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) {
// 如果父類加載器抛出ClassNotFoundException,
// 說明父類加載器無法完成加載請求
}
if (c == null) {
// 在父類加載器無法加載的時候,再調用本類的findClass方法進行類加載請求
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
// 目前類加載器是該類的define class loader
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
破壞雙親委派模型
線程上下文加載器
如上所述,雙親委派模型很好得解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),如果基礎類又要回調使用者的類該怎麼辦?一個非常經典的例子就是SQL的驅動管理類——java.sql.DriverManager。
java.sql.DriverManager是Java的标準服務,該類放在rt.jar中,是以是由啟動類加載器加載的,但是在應用啟動的時候,該驅動類管理是需要加載由不同資料庫廠商實作的驅動,但是啟動類加載器找不到這些具體的實作類,為了解決這個問題,Java設計團隊提供了一個不太優雅的設計:線程上下文加載器(Thread Context ClassLoader)。這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立線程時候它還沒有被設定,就會從父線程中繼承一個,如果再應用程式的全局範圍都沒有設定過的話,那這個類加載器就是應用程式類加載器。
有了線程上下文加載器,就可以解決上面的問題——父類加載器需要請求子類加載器完成類加載的動作,這種行為實際上就是打破了雙親委派的加載規則。
源碼分析
接下來,我們以java.sql.DriverManager為例,看下線程上下文加載器的用法,在java.sql.DriverManager類的下面這個靜态塊中,是JDBC驅動加載的入口。
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
順着loadInitialDrivers()方法往下看,使用線程上下文加載器的地方在ServiceLoader.load裡
private static void loadInitialDrivers() {
// ……省去别的代碼
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) {
// Do nothing
}
return null;
}
});
//…… 省去别的代碼
ServiceLoader.load方法的代碼如下,JDBC的sqlDriverManager就是這裡獲得的上下文加載器來驅動使用者代碼加載指定的類的。
public static <S> ServiceLoader<S> load(Class<S> service) {
// 擷取目前線程中的上下文類加載器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
那麼這個上下文加載器是什麼時候設定進去的呢?前面我們提到了:
這個類加載器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立線程時候它還沒有被設定,就會從父線程中繼承一個,如果再應用程式的全局範圍都沒有設定過的話,那這個類加載器就是應用程式類加載器。
看下setContextClassLoader()方法别誰調用了,最終我們在Launcher中找到了如下代碼:
public class Launcher {
//……省去别的代碼
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
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);
//……省去别的代碼
}
}
總結
這篇文章我們複習了類加載器的雙親委派模型、雙親委派模型的工作過程,以及打破雙親委派模型的必要性和源碼分析。在第一部分的結尾,我們還示範了Arthas中關于類加載器的指令的用法,在實際排查問題時可以考慮使用。
參考資料
- https://www.journaldev.com/349/java-classloader#java-classloader-hierarchy
- https://www.cnblogs.com/joemsu/p/9310226.html
- 《深入了解Java虛拟機》
- 《碼出高效Java開發手冊》
本号專注于後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收獲。