前言
Java里有如下幾種類加載器
- 啟動類加載器:負責加載支撐JVM運行的位于JRE的lib目錄下的核心類庫比如 rt.jar、charsets.jar等。
- 擴展類加載器(ExtClassLoader):負責加載支撐JVM運行的位于JRE的lib目錄下的ext擴展目錄中的JAR類包。
- 應用程序類加載器(AppClassLoader):負責加載ClassPath路徑下的類包,主要就是加載你自己寫的那些類。
- 自定義加載器:負責加載用戶自定義路徑下的類包。
通過以下實例來了解各個類加載器:
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(Object.class.getClassLoader());
// java提供的與DNS服務交互的api
System.out.println(DNSNameService.class.getClassLoader());
System.out.println(ClassLoaderTest.class.getClassLoader());
}
}
運行結果如下:
null
sun.misc.Launcher$ExtClassLoader@6d6f6e28
sun.misc.Launcher$AppClassLoader@58644d46
啟動類加載器是在有jvm底層創建的實例,所以在獲取時為null,Object類是有啟動類加載器進行加載的,所以獲取其加載器時為null,而DNSNameService為JAVA_HOME/jre/lib目錄下ext文件夾在的dnsns.jar包中的類,由擴展類加載器(ExtClassLoader)加載。而自己編寫的類ClassLoaderTest 則由AppClassLoader進行加載。
Java中各個類加載器的層次關系
在上面已經介紹過,java的類加載器也是普通的類,ExtClassLoader和AppClassLoader均是URLClassLoader的子類,而URL的繼承關系如下:
那么AppClassLoader和ExtClassLoader為ClassLoader的子類。在上面已經已經介紹過在jvm啟動時會通過sun.misc.Launcher的getLauncher方法從而獲取Launcher的實例,那么在這個過程中Launcher會通過構造方法創建該類的實例。sun.misc.Launcher的構造方法如下:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 創建ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 創建AppClassPoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 省略代碼 ....
}
}
通過分析sun.misc.Launcher構造方法我們知道在sun.misc.Launcher類的實例創建是會創建AppClassLoader實例和ExtClassLoader實例。同時由于兩個類加載器均繼承自ClassLoader,而ClassLoader中有一個ClassLoader的全局變量parent,該類類型也是ClassLoader:
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// 省略代碼 ....
}
而在sun.misc.Launcher創建時,實例化ExtClassLoader和AppClassLoader時均指定其parent屬性分別為null和ExtClassLoader。那么java中的類加載器的機構就如下:
自定義類加載器
自定義類加載器需要繼承 java.lang.ClassLoader 類,該類有兩個核心方法,一個是 loadClass(String, boolean),實現了雙親委派機制,大體邏輯
首先,檢查一下指定名稱的類是否已經加載過,如果加載過了,就不需要再 加載, 直接返回。
如果此類沒有加載過,那么,再判斷一下是否有父加載器;如果有父加載器, 則由父加載器加載(即調用parent.loadClass(name, false);).或者是調用 Bootstrap類加載器來加載。
如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調用當前類 加載器 的findClass方法來完成類加載。還有一個方法是findClass,默認 實現是拋出異常,所以我們自定義類加載器主要是重寫 findClass方法。
接下來看一個示例,首先我們編寫需要自定義類加載器加載的類,如下:
package com.dp.jvm;
import java.io.PrintStream;
public class User
{
public void say()
{
System.out.println("hello");
}
}
需要注意的是,該類編寫完成需要在工程中刪除,避免AppClassLoader加載。編譯完成后將該類的class文件放置指定的目錄下:
然后編寫自定義的類加載器,代碼如下:
class MyClassLoader extends ClassLoader{
private final String path;
MyClassLoader(String path) {
this.path = path;
}
/**
* 重寫ClassLoader的findClass方法,獲取到類的Class對象
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] byteArrayFromClassName = getByteArrayFromClassName(name);
return defineClass(name, byteArrayFromClassName, 0, byteArrayFromClassName.length);
}
/**
* 通過類的全限定名稱獲取到類的二進制數據
* @param name
* @return
*/
private byte[] getByteArrayFromClassName(String name) {
String classPath = convertNameToPath(name);
byte[] data = null;
int off = 0;
int length = 0;
try(BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(classPath))) {
data = new byte[bufferedInputStream.available()];
while ((length = bufferedInputStream.read(data, off, data.length - off)) > 0) {
off += length;
}
} catch (Exception ex) {
ex.printStackTrace();
}
return data;
}
/**
* 通過類的全限定名稱獲取到對應類文件的的字節碼文件路徑
* @param name
* @return
*/
private String convertNameToPath(String name) {
String relativePath = name.replace(".", File.separator);
String absolutePath = path + File.separator + relativePath + ".class";
return absolutePath;
}
}
編寫測試類,通過使用自定義的類加載將User加載并實例化,然后調用其say方法,如下:
public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("F:\\test");
Class<?> clazz = myClassLoader.loadClass("com.dp.jvm.User");
Object o = clazz.newInstance();
Method say = clazz.getDeclaredMethod("say");
say.invoke(o);
}
}
通過上面了實例,簡單的實現了一個自定義的類加載器。接留下來了解一下類加載器的雙親委派機制。
雙親委派機制
JVM類加載器是有親子層級結構的,如下圖:
需要注意的是,這里的額親子層級結構不是指的java中的繼承關系,而是每一個類加載實現類都具有一個parent全局變量,而該全局變量的類型為ClassLoader。這里可能有一個疑問,在自定義類加載器中并未看到名稱parent的全局變量。這是因為這個全局變量是在ClassLoader中定義聲明的。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// 省略代碼 ....
還有一個問題也需要說明一下,我們自定義的類加載器的parent屬性是如何設置的呢?怎么知道設置的為AppClassLoader呢?因為自定義的類加載器繼承自ClassLoader,而ClassLoader中有一個無參的構造函數,如下:
protected ClassLoader() {
//調用有參構造函數
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
//設置父加載器
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
從ClassLoader的實現來看,通過getSystemClassLoader()方法獲取系統類加載器然后將其賦值給parent屬性。那么來看一下getSystemClassLoader()具體實現:
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
// 初始化系統類加載器
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
在getSystemClassLoader方法中獲取sun.misc.Lanucher實例(單例),然后調用其getClassLoader方法獲取系統類加載器,然后設置給parent方法。最后來看一下sun.misc.Lanucher的getClassLoader方法:
public ClassLoader getClassLoader() {
return this.loader;
}
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);
}
}
結合sun.misc.Lanucher的getClassLoader和構造方法可知系統類加載器就是AppClassLoader。
在了解了jvm中類加載器的組成結構后,我們再來看一下jvm中各個類加載器的組成的結構:
在了解了jvm中各個類加載器的層次結構之后,加下來來解析雙親委派機制就相對來說簡單多了,首先從雙親委派的流程說起。
雙親委派流程
雙親委派流程如下:
加載某個類時會先委托父加載器尋找目標類,找不到再委托上層父加載器加載,如果所有父加載器在自己的加載類路徑下都找不到目標類,則在自己的類加載路徑中查找并載入目標類。
比如我們的PrintTest 類,最先會找應用程序類加載器加載,應用程序類加載器會先委托擴展類加載器加載,擴展類加載器再委托啟動類加載器,頂層啟動類加載器在自己的類加載路徑里找了半天沒找到PrintTest 類,則向下退回加載PrintTest 類的請求,擴展類加載器收到回復就自己加載,在自己的類加載路徑里找了半天也沒找到PrintTest 類,又向下退回PrintTest 類的加載請求給應用程序類加載器,應用程序類加載器于是在自己的類加載路徑里找Math類,結果找到了就自己加載了。
雙親委派機制說簡單點就是,先找父親加載,不行再由兒子自己加載。
那么為什么要設置雙親委派機制呢?
- 沙箱安全機制:自己寫的java.lang.String.class類不會被加載,這樣便可以防止核心API庫被隨意篡改。
- 避免類的重復加載:當父親已經加載了該類時,就沒有必要子ClassLoader再加載一次,保證被加載類的唯一性
雙親委派機制源碼剖析
雙親委派的原理體現在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 {
// 判斷當前類加載器是否設置了父加載器,設置了則
// 調用父加載器的loadClass進行加載,如果父加載也是ClassLoader
// 的子類則會再次進入該方法,判斷是否有父類加載器,依次遞歸
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 當類加載沒有設置parent父加載,那么就使用啟動類加載器加載
// 由于啟動類加載器是底層創建的實例,所以該方法會調用本地
// native方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 向上委派所有父加載器仍然沒有加載到參數類,那么調用當前
// 類加載器進行類的加載
long t1 = System.nanoTime();
c = findClass(name);
// ... 省略
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
打破雙親委派
以Tomcat類加載為例,Tomcat 如果使用默認的雙親委派類加載機制行不行?
我們思考一下:Tomcat是個web容器, 那么它要解決什么問題:
- 一個web容器可能需要部署兩個應用程序,不同的應用程序可能會依賴同一 個第三方類庫的不同版本,不能要求同一個類庫在同一個服務器只有一份, 因此要保證每個應用程序的類庫都是獨立的,保證相互隔離。比如:在tomcat容器中存在存在兩個應用A和B,A使用的是Spring4,而應用B使用的是Spring5,如果使用雙親委派,那么可能會導致版本沖突從而報錯,如果在版本4中不存在x方法,但是先加載了版本4的字節碼,那么版本5的就不會在加載了(類限定名相同),那么在程序B中調用x方法則會拋出方法不存在異常。
- 部署在同一個web容器中相同的類庫相同的版本可以共享。否則,如果服務器有10個應用程序,那么要有10份相同的類庫加載進虛擬機。我們常見的web應用中的Servlet依賴,一般在maven中依賴作用于都是provided的,web程序都是使用的容器的Serlvert,如果都是各自的那么造成類的重復加載。
- web容器也有自己依賴的類庫,不能與應用程序的類庫混淆。基于安全考慮,應該讓容器的類庫和程序的類庫隔離開來。
- web容器要支持jsp的修改,我們知道,jsp文件最終也是要編譯成class文件才能在虛擬機中運行,但程序運行后修改jsp已經是司空見慣的事情,web容器需要支持 jsp修改后不用重啟.了解jsp機制的都知道,是將jsp解析成一個對應的Servlet(就是常說的一個jsp就是一個servlet),jsp就是通過動態生成.class文件從而實現動態資源的。
再看看我們的問題:
Tomcat 如果使用默認的雙親委派類加載機制行不行?
- 第一個問題,如果使用默認的類加載器機制,那么是無法加載兩個相同類庫的不同版本的,
默認的類加器是不管你是什么版本的,只在乎你的全限定類名,并且只有一份。 - 第二個問題,默認的類加載器是能夠實現的,因為他的職責就是保證唯一性。
- 第三個問題和第一個問題一樣。
- 我們再看第四個問題,我們想我們要怎么實現jsp文件的熱加載,jsp文件其實也就是class文件,那么如果修改了,但類名還是一樣,類加載器會直接取方法區中已經存在的,修改后的jsp是不會重新加載的。那么怎么辦呢?我們可以直接卸載掉這jsp文件的類加載器,所以你應該想到了,每個jsp文件對應一個唯一的類加載器,當一個jsp文件修改了,就直接卸載這個jsp類加載器。重新創建類加載器,重新加載jsp文件。
最后
感謝你看到這里,看完有什么的不懂的可以在評論區問我,覺得文章對你有幫助的話記得給我點個贊,每天都會分享java相關技術文章或行業資訊,歡迎大家關注和轉發文章!