類載入器深入詳解
類載入器
JAVA虛擬機器自身提供的載入器
啟動類載入器(Bootstrap ClassLoader)
主要負責載入 ${JAVA_HOME}/lib 目錄下的類庫
擴充套件類載入器(Extension ClassLoader)
主要負責載入 ${JAVA_HOME}/lib/ext 目錄下的類庫
應用程式類載入器(Application ClassLoader)
它主要負責載入使用者類路徑(classpath)上指定的類庫 舉個列:
package test.jvm;
/**
* @author long
* @Desc
* @date 2018/10/15 下午9:14
*/
public class ClassLoaderTest {
private String getClassPath() {
return this.getClass().getResource("/").getPath();
}
public static void main(String[] args) throws InterruptedException {
ClassLoaderTest classLoaderTest = new ClassLoaderTest();
System.out.println(classLoaderTest. getClassPath() );
}
}
輸出:/Users/hejianglong/study/bingfabiancheng/out/production/bingfabiancheng/
意思AppClassLoader會去載入我建立在 study目錄下的 bingfabiancheng這個專案的所有的自定義類。
使用者自定義的載入器
什麼時候需要自定義載入器呢? 比如當我們的class檔案存在遠端伺服器上,並且將訪問地址存入了資料庫,當我們從資料庫獲取對應的class地址的時候,比如 http://www.ab.com/static/Test.class ,AppClassLoader也無法載入,這個時候就需要我們來實現自定義載入器來進行載入了,本文暫時不講自定義載入器,改天我會單獨寫一篇文章來進行描述。
雙親委派模型
上面的內容我們瞭解到了有哪些類載入器,分別用於載入哪一塊都內容,那麼它是如何實現類載入的呢,就是通過雙親委派模型演算法。 先來看一張圖大致理解一下
主要思路
-
當類載入器收到類載入請求的時候,發現它還有父類載入器就委派給父類載入器,依次往上,這樣的話最頂層肯定都是BootstrapClassLoader進行類載入
-
由於BootstrapClassLoader只加載${JAVA_HOME}/lib下面都類庫,其它的它載入不了比如test.jvm.ClassLoaderTest 它就返回給ExtClassLoader給他說我載入不了,交給你了
-
由於ExtClassLoader只負責載入${JAVA_HOME}/lib/ext 目錄,它也載入不了,就返回給Application ClassLoader
-
因為Application ClassLoader負責載入使用者類路徑(classpath)下的類庫,他能載入成功
-
此處未涉及到自定義都類載入器,自定義再起在AppClassLoader載入不了都時候使用
原始碼分析
loadClass(String name, boolean resolve)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 同步載入class
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.
// 無法被啟動類載入器載入的交給findClass
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;
}
}
看了這段程式碼結合上面的文字描述相信大家基本上都能明白雙親委派模型的大致實現思路 下面我從原始碼上來分析幾個關鍵的方法
1. getClassLoadingLock(String name)
2. findClass(String name);
3. resolveClass(Class c);
getClassLoadingLock(String name)
將鎖物件放入parallelLockMap中,放入成功則返回 lock = null,如果之前已經存則返回已經被載入的物件 那麼此處put失敗,返回之前的鎖物件,這樣就保證瞭如果是並行載入那麼會對一個物件進行加鎖同步
/**
* Returns the lock object for class loading operations.
* For backward compatibility, the default implementation of this method
* behaves as follows. If this ClassLoader object is registered as
* parallel capable, the method returns a dedicated object associated
* with the specified class name. Otherwise, the method returns this
* ClassLoader object.
* 主要意思,如果是單個類載入就返回鎖物件, 如果是並行的載入就返回類載入的物件
*/
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
// 獲取一個鎖物件
Object newLock = new Object();
// 將鎖物件放入parallelLockMap中,放入成功則返回 lock = null,如果之前已經存則返回已經被載入的物件
// 那麼此處put失敗,返回之前的鎖物件,這樣就保證瞭如果是並行載入那麼會對一個物件進行加鎖同步
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
findClass(String name)
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
這裡它有非常多都classLoader實現,實際上載入使用者類路徑類庫呼叫的是 URLClassLoader -> findClass
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
這裡主要是defineClass(name, res)這行程式碼 它將根據類的全路徑進行讀取,將其載入到方法區,並生成一個以class物件入口,用來訪問其類資訊, 此處需要主要的是,這時類似不能被正常使用,因為還沒有進行連線,初始化等過程可能還會存在錯誤。
resolveClass(Class c)
相信大家進行斷點 ClassLoader都loadClass(String name, boolean resolve)的時候也注意到了 這裡都 resolve預設為false的。 那麼也就意味著不會執行以下程式碼
if (resolve) {
resolveClass(c);
}
那麼它到底是用來做什麼的呢,我們來看看他都原始碼及其描述
/**
* Links the specified class. This (misleadingly named) method may be
* used by a class loader to link a class. If the class <tt>c</tt> has
* already been linked, then this method simply returns. Otherwise, the
* class is linked as described in the "Execution" chapter of
* <cite>The Java™ Language Specification</cite>.
*/
protected final void resolveClass(Class<?> c) {
// 這是一個native方法
resolveClass0(c);
}
大致意思就是,這個方法目的是進行載入步驟的 “連線”(linking),如果已經連線過了那麼他就直接返回,否則就會執行一遍連線過程。 如果不瞭解類載入,連線這些的可以看下這篇文章 JAVA虛擬類載入過程
我們知道linking(連線)包含 驗證,準備,解析(可能會延遲解析) 然後還要進行初始化後才能正常使用 意思就是loadClass(name, false) 返回的類,只是已經被正確載入的類,還他不一定正確,因為還沒有連線過。
為什麼要這麼做呢?個人猜測 這就相當於根據 resolve欄位為true或者false選擇延遲連結,好處是什麼呢? 如果為false, 可以提升我們應用都啟動速度算是其中一點把,因為少了連結這個步驟。但是應該會在初次使用類都時候速度回降低因為還需要進行連線
這裡我們來看一下<<JAVA虛擬機器規範8>>關於何時進行連線的描述
1. 在類或介面被連線前,他必須被成功都載入過
2. 在類或介面初始化前,他必須被成功都驗證及準備過
3. 若程式執行了某種可能需要直接或間接連線一個類或介面的動作,而連線類或介面都過程中又檢測到了錯誤,則錯誤的丟擲點應該是執行動作的那個點
由於筆者技術水平有限,如果有錯誤歡迎大家在評論中指正,謝謝。