更多請移步: 我的博客
初識ClassLoader
在開發中有時會碰到ClassNotFoundException。這個異常和ClassLoader有着密切的關係。
我們常使用instanceof關鍵字判斷某個對象是否屬於指定Class創建的對象實例。如果對象和Class不屬同一個加載器加載,那麼instanceof返回的結果一定是false。
GC Root有一種叫做System Class,官方解釋“Class loaded by bootstrap/system class loader. For example, everything from the rt.jar like java.util.* .”,大意是:被bootstrap/system加載器加載的類,比如,像java.util.*這些來自rt.jar的類。
GC時對Class的卸載,需要滿足的條件如下:
類需要滿足以下3個條件才能算是“無用的類”
- 該類所有的實例已經被回收
- 加載該類的ClassLoder已經被回收
- 該類對應的java.lang.Class對象沒有任何對方被引用
ClassLoader簡介
我們的Java應用程序都是由一系列編譯爲class文件組成,JVM在運行的時候會根據需要(比如:我們需要創建一個新對象,但是該對象的Class定義並未在Perm區找到)將應用需要的class文件找到並加載到內存的指定區域供應用使用,完成class文件加載的任務就是由ClassLoader完成。
ClassLoader類的基本職責就是根據一個指定的類的名稱,找到(class可能來源自本地或者網絡)或者生成其對應的字節代碼,然後從這些字節代碼中定義出一個java.lang.Class類的一個實例。除此之外,ClassLoader還負責加載 Java 應用所需的資源,如圖像文件和配置文件等。
在JVM中每個ClassLoader有各自的命名空間,不同的ClassLoader加載的相同class文件創建的Class實例被認爲是不相等的,由不相等的Class創建的對象實例無法相互強制轉型,如開頭所提,當我們使用instanceof關鍵字判斷時需要注意。
雙親委派模型
雙親委派模型很好理解,直接上代碼。
/**
* 使用指定的二進制名稱來加載類。默認的查找類的順序如下:
* 調用findLoadedClass(String) 檢查這個類是否被加載過;
* 調用父加載器的loadClass(String),如果父加載器爲null,使用虛擬機內置的加載器代替;
* 如果父類未找到,調用findClass(String)方法查找類。
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
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 thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
從源碼中我們看到有三種類加載器:
引導類加載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼(C++)來實現的,並不繼承自java.lang.ClassLoader。負責將${JAVA_HOME}/lib目錄下和-Xbootclasspath參數所指定的路徑中的,並且是Java虛擬機識別的(僅按照文件名識別,如rt.jar,不符合的類庫即使放在lib下也不會被加載)類庫加載到JVM內存中,引導類加載器無法被Java程序直接引用;
擴展類加載器(extensions class loader):它用來加載 Java 的擴展庫(${JAVA_HOME}/ext),或者被java.ext.dirs系統變量所指定的路徑中的所有類庫;
系統類加載器(system class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載Java類。一般來說,Java 應用的類都是由它來完成加載的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。
public class ClassLoaderTree {
/**
* 輸出:
* sun.misc.Launcher$AppClassLoader@18b4aac2
* sun.misc.Launcher$ExtClassLoader@5305068a
* null
* @param args
*/
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTree.class.getClassLoader();
while (loader!=null){
System.out.println(loader.toString());
loader = loader.getParent();
}
System.out.println(loader);
}
}
每個Java類都維護着一個指向定義它的類加載器的引用,通過getClassLoader()方法就可以獲取到此引用。通過調用getParent()方法可以得到加載器的父類,上述代碼輸出中,AppClassLoader對應系統類加載器(system class loader);ExtClassLoader對應擴展類加載器(extensions class loader);需要注意的是這裏並沒有輸出引導類加載器,這是因爲有些JDK的實現對於父類加載器是引導類加載器。這些加載器的父子關係通過組合實現。
爲什麼要雙親委派
- 避免重複加載。當父親已經加載了該類,子類就沒有必要再加載一次;
- 安全。如果不使用這種委託模式,那我們就可以使用自定義的String或者其他JDK中的類,存在非常大的安全隱患,而雙親委派使得自定義的ClassLoader永遠也無法加載一個自己寫的String。
創建自定義ClassLoader
自定義的類加載器只需要重寫findClass(String name)方法即可。java.lang.ClassLoader封裝了委派的邏輯,爲了保證類加載器正常的委派邏輯,儘量不要重寫findClass()方法。
public class FileSystemClassLoader extends ClassLoader {
private String rootDir;
public FileSystemClassLoader(String rootDir){
this.rootDir = rootDir;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData == null){
throw new ClassNotFoundException();
}
else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1){
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}
實驗
更多實驗代碼放在github上:
https://github.com/Childe-Chen/goodGoodStudy/tree/master/src/main/java/com/cxd/classLoader
public class TestClassIdentity {
public static void main(String[] args) {
FileSystemClassLoader fileSystemClassLoader = new FileSystemClassLoader("/Users/childe/Documents/workspace/goodGoodStudy/target/classes");
try {
Class<?> c = fileSystemClassLoader.findClass("com.cxd.classLoader.Sample");
//forName會執行類中的static塊(初始化)
Class<?> c1 = Class.forName("com.cxd.classLoader.Sample");
System.out.println(c1.isAssignableFrom(c));
//運行時拋出了 java.lang.ClassCastException異常。雖然兩個對象 o1 o2的類的名字相同,但是這兩個類是由不同的類加載器實例來加載的,因此不被 Java 虛擬機認爲是相同的。
//不同的類加載器爲相同名稱的類創建了額外的名稱空間。相同名稱的類可以並存在 Java 虛擬機中,只需要用不同的類加載器來加載它們即可。
// 不同類加載器加載的類之間是不兼容的,這就相當於在 Java 虛擬機內部創建了一個個相互隔離的 Java 類空間。這種技術在許多框架中都被用到
Object o = c.newInstance();
Method method = c.getMethod("setSample", java.lang.Object.class);
Object o1 = c1.newInstance();
method.invoke(o,o1);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public class Sample {
private Sample instance;
static {
System.out.println("static");
}
public void setSample(Object instance) {
this.instance = (Sample) instance;
}
}
打破雙親委派模型
沒有完美的模型,雙親委派在面對SPI時,不得不做出了特例或者說改進。我們知道Java提供了很多服務提供者接口(Service Provider Interface,SPI),允許第三方爲這些接口提供實現。常見的有JDBC、JCE、JNDI、JAXP 和 JBI 等。SPI的接口由Java核心庫定義,而其實現往往是作爲Java應用所依賴的 jar包被包含到CLASSPATH裏。而SPI接口中的代碼經常需要加載具體的實現類。那麼問題來了,SPI的接口是Java核心庫的一部分,由引導類加載器加載;SPI的實現類是由系統類加載器加載,引導類加載器無法找到SPI的實現類,因爲依照雙親委派模型,BootstrapClassloader無法委派AppClassLoader來加載類。
爲了解決這個問題,Java引入了線程上下文類加載器,在Thread中聚合了contextClassLoader,通過Thread.currentThread().getContextClassLoader()獲得。原始線程的上下文ClassLoader通常設定爲用於加載應用程序的類加載器。也就是說父加載器可以通過縣城上下文類加載器可以獲得第三方對SPI的實現類。
以Java鏈接Mysql爲例,看下Java如何來加載SPI實現。
// 註冊驅動,forName方法會初始化Driver,初始化塊中向DriverManager註冊驅動
Class.forName("com.mysql.jdbc.Driver").getInstance();
String url = "jdbc:mysql://host:port/db";
// 通過java庫獲取數據庫連接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
com.mysql.jdbc.Driver是java.sql.Driver的一種實現。
package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
// 向DriverManager註冊驅動
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
...
}
接下來我們調用getConnection就進入了本小結的關鍵點。
// Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* 再次強調下:原始線程的上下文ClassLoader通常設定爲用於加載應用程序的類加載器
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
//caller由Reflection.getCallerClass()得到,而調用方是java.sql.DriverManager,所以getClassLoader()是引導類加載器,也就是null
//所以此處使用線程上下文加載器來加載實現類
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
// isDriverAllowed中使用給定的加載器加載指定的驅動
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
總結&擴展
- 雙親委派作爲基本模型,隔離了不同的調用者,保證了程序的安全。
- 線程上線文加載器與其說破壞了雙親委派倒不如說是擴展了雙親委派的能力,使其有更好的通用性。
- Tomcat、Jetty等Web容器都是基於雙親委派模型來做資源的隔離。
- Spring在設計中也考慮到了類加載的問題,詳細可見:
org.springframework.web.context.ContextLoader.initWebApplicationContext(…)。
參考
http://www.infocool.net/kb/Tomcat/201609/193323.html
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/
http://github.thinkingbar.com/classloader/