以前的文章中,介紹了class的字節碼靜態結構,這些類須要jvm加載到其在內存中分配的運行時數據區纔會生效,這個過程包含:加載 -> 連接 -> 初始化
幾個階段,其中連接階段又有驗證 -> 準備 -> 解析
三個部分,接下來我會用三篇文章分別詳細介紹這三個階段,本文先介紹jvm類的加載以及雙親委派模型的概念。java
注意:類加載包含了從字節碼流到jvm方法區
java.lang.Class
對象建立並初始化整個過程,而本文介紹的類的加載只是其中一個階段shell
本文內容基於hotspot jvm,運行時Class對象就存儲在Method Area中設計模式
何時開始加載一個類的字節碼呢?對此jvm規範並無給出明肯定義,可是jvm規範明確規定了類初始化的時機,根據類的加載發生在其初始化以前,能夠反推出類其加載的觸發條件。api
注:本文的類是泛指,還包括接口等數組
jvm規範定義了有且僅有5種狀況下若是類尚未初始化,會觸發類的初始化:緩存
java.lang.invoke.MethodHandle
實例解析結果爲REF_getStatic
, REF_putStatic
, REF_invokeStatic
方法句柄時上面幾種狀況都很好理解,當前類引用了某個類而且使用了它,天然須要初始化,也天然要加載它,能夠經過-XX:+TraceClassLoading
查看加載的類。關於初始化,之後的文章會單獨詳細介紹tomcat
類的加載就是把一個類的字節碼靜態結構經過jvm加載,並建立一個對應的java.lang.Class
對象,存儲在本身的運行時方法區內存空間,此後,這個類的數據便經過這個Class對象來訪問,包括其類field,方法等。網絡
字節碼不只是侷限於本地文件系統中的文件,也多是在內存中(動態生成),網絡上,壓縮包(jar, war)等,而類加載器的職責就是從這些地方加載字節碼到jvm中。架構
類加載器按其實現能夠分爲兩類:引導類加載器(Bootstrap Class Loader),用戶類加載器(User-defined Class Loader)app
引導類加載器:加載$JAVA_HOME/jre/lib/
下核心類庫,如rt.jar,hotspot jvm中由C++實現
用戶類加載器:全部用戶類加載器都繼承了java.lang.ClassLoader
抽象類,sun提供了兩個用戶類加載器,咱們也能夠定義本身的類加載器
擴展類加載器(ExtClassLoader):sun.misc.Launcher$ExtClassLoader
,負責加載$JAVA_HOME/jre/lib/ext
下的一些擴展類
應用類加載器(AppClassLoader):可由ClassLoader.getSystemClassLoader()
方法得到,也稱系統類加載器,負責加載用戶(classpath中)定義的類。
自定義類加載器(Custom ClassLoader):用戶也能夠定義本身的類加載器,實現一些定製的功能
關於類加載器補充幾點:
java.lang.ClassLoader
抽象類,該類又個private final ClassLoader parent
字段表示一個加載器的父加載器(設計模式中推薦使用這種組合的方式來代替繼承),這是實現雙親委派模型的關鍵。rt.jar
,因此不會加載用戶的類newarray
指令進行初始化時,若是數組的元素類型不是基本類型(如int[]),而是引用類型(如Integer[]),則會先加載基本類型,這可能由引導類加載器或用戶類加載器加載,具體看引用類型是什麼。下面經過實例來看一下:
/** * 自定義類加載器,重寫loadClass,優先在當前目錄加載 */
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
try {
InputStream is = new FileInputStream("./" + name + ".class");
byte[] data = new byte[is.available()];
is.read(data);
return defineClass(name, data, 0, data.length);
} catch (IOException e) {
return super.loadClass(name);
}
}
}
複製代碼
public class Callee {
public Callee() {
System.out.println("Callee class loaded by " + this.getClass().getClassLoader().getClass().getName());
}
}
複製代碼
/** * Run: javac MyClassLoader.java Callee.java Test.java && java Test * / public class Test { public static void main(String[] args) throws Exception { ClassLoader myClassLoader = new MyClassLoader(); Class<?> calleeClass = myClassLoader.loadClass("Callee"); //輸出:calleeClass == Callee.class ? false System.out.println("calleeClass == Callee.class ? " + (calleeClass == Callee.class)); //輸出:Callee class loaded by sun.misc.Launcher$AppClassLoader Callee.class.newInstance(); //輸出:Callee class loaded by MyClassLoader Object calleeObj = calleeClass.newInstance(); } } 複製代碼
能夠看出,雖然是同一個類Callee
,但因爲是不一樣類加載器加載,因此Class實例並非同一個。
所謂雙親委派模型是指一個類加載器在加載某個類時,首先把委派給父加載器去加載,父加載器又委派給它的父加載器加載,如此頂層的引導類加載器爲止,若是其父加載器在其搜索範圍沒有找到相應類,則嘗試本身加載。
從雙親委派模型的定義能夠看出,它要求每一個加載器都有一個父加載器,若是某個類加載器的父加載器爲null,則搜索引導類加載器是否加載過它要加載的類。
能夠看出首先接收加載請求的類加載器並不必定真正加載類,可能由它的父加載器完成加載,接收加載請求的類加載器叫作初始類加載器(initiating loader)
,而完成加載的類加載器叫作定義類加載器(defining loader)
,初始類加載器和定義類加載器可能相同也可能不一樣。
若是兩個類:D引用了C,L1做爲D的定義類加載器,在解析D時會去加載C,這個加載請求由L1接收,假設C由另外一個加載器L2加載,則L1最終將加載請求委託給L2,L1就稱爲C的初始加載器,L2是C的定義類加載器。
下面看看ClassLoader怎麼實現雙親委派加載的:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 一個類的加載是放在代碼同步塊裏邊的,因此不會有同一個類加載屢次
synchronized (getClassLoadingLock(name)) {
// 首先檢查該類是否已加載過
Class<?> c = findLoadedClass(name);
// 若是緩存中沒有找到,則按雙親委派模型加載
if (c == null) {
try {
if (parent != null) {
// 若是父加載器不爲null,則代理給父加載器加載
// 父加載器在本身搜索範圍內找不到該類,則拋出ClassNotFoundException
c = parent.loadClass(name, false);
} else {
// 若是父加載器爲null,則從引導類加載器加載過的類中
// 找是否加載過此類,找不到返回null
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 存在父加載器但父加載器沒有找到要加載的類觸發此異常
// 只捕獲不處理,交給字加載器自身去加載
}
if (c == null) {
// 若是從父加載器到頂層加載器(引導類加載器)都找不到此類,則本身來加載
c = findClass(name);
}
}
// 若是resolve指定爲true,則當即進入連接階段
if (resolve) {
resolveClass(c);
}
return c;
}
}
複製代碼
經過源碼能夠看出,全部的類都優先委派給父加載器加載,若是父加載器沒法加載,則本身來加載,邏輯很簡單,這樣作的好處是不用層次的類交給不一樣的加載器去加載,如java.lang.Integer
最終都是由Bootstrap ClassLoader來加載的,這樣只會有一個相同類被加載。
再來講說裏邊調用的幾個方法:
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
Object newLock = new Object();
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
複製代碼
該方法很簡單,parallelLockMap是一個ConcurrentHashMap<String, Object>
map對象,若是當前classloader註冊爲可並行加載的,則爲每個類名維護一個鎖對象供synchronized
使用,可並行加載不一樣類,不然以當前classloader做爲鎖對象,只能串行加載。
private Class<?> findBootstrapClassOrNull(String name)
{
if (!checkName(name))
return null;
return findBootstrapClass(name);
}
複製代碼
private native Class<?> findBootstrapClass(String name);
複製代碼
findBootstrapClass是jvm原生實現,查找Bootstrap ClassLoader已加載的類,沒有則返回null
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
複製代碼
findClass
交給子加載器實現,咱們通常重寫該方法來實現本身的類加載器,這樣實現的類加載器也符合雙親委派模型。固然,雙親委派的邏輯都是在loadClass
實現的,能夠本身重寫loadClass
來打破雙親委派邏輯。
自定義類加載器:
/** * Run: javac MyClassLoader.java Callee.java Test.java && java Test */ public class MyClassLoader extends ClassLoader { @Override public Class<?> findClass(String name) throws ClassNotFoundException { try { InputStream is = new FileInputStream("./" + name + ".class"); byte[] data = new byte[is.available()]; is.read(data); return defineClass(name, data, 0, data.length); } catch (IOException e) { return super.loadClass(name); } } } public static void main(String[] args) throws Exception { ClassLoader myClassLoader = new MyClassLoader(); Class<?> callerClass = myClassLoader.loadClass("Callee"); // 輸出:Callee class loaded by sun.misc.Launcher$AppClassLoader callerClass.newInstance(); } 複製代碼
能夠看出,只需吧前面的示例方法名改成findClass
就能夠了,並且能夠看到是由應用類加載器負責加載的(默認父加載器是AppClassLoader),符合雙親委派模型。
再來作個實驗:
// 讓自定義類加載器加載/tmp目錄下的類
InputStream is = new FileInputStream("/tmp/" + name + ".class");
複製代碼
把剛編譯的Callee.class
移動至/tmp
下(注意:當前目錄不要也保留一份):
mv Callee.class /tmp
複製代碼
再次編譯運行:
javac MyClassLoader.java && java Test
複製代碼
結果:
Callee class loaded by MyClassLoader
複製代碼
Callee
變成由自定義類加載器加載了,由於向上委託時都找不到該類,自定義加載器findClass
方法起了做用。
再來作個有趣的實驗:
定義一個類Caller
裏邊調用了Callee
:
public class Caller {
public Caller() {
System.out.println("Caller class loaded by " + this.getClass().getClassLoader().getClass().getName());
Callee callee = new Callee();
}
}
複製代碼
修改Test.java,加載Caller
Class<?> callerClass = myClassLoader.loadClass("Caller");
複製代碼
再次編譯運行:
javac MyClassLoader.java Caller.java Test.java
mv Callee.class /tmp # 保證當前目錄下沒有Callee.class,/tmp下有
java Test
複製代碼
爲何/tmp下有Callee.class
但沒有加載到呢?其實很好理解:輸出第一句看出AppClassLoader
加載了Caller.class
,做爲它的定義類加載器,當Caller中使用了Callee須要加載Callee.class
的時候,AppClassLoader
就會做爲Callee.class
的初始加載器去加載它,根據雙親委派模型,最後AppClassLoader
調用本身的findClass
嘗試本身加載,classpath下沒有這個類,確定找不到~
這個例子還能夠看出:真正去加載類的類加載器(調用findClass方法)找不到類拋出
ClassNotFoundException
,此異常被封裝成NoClassDefFoundError
拋出給使用的地方(初始類加載器),這種錯誤很常見。
雙親委派模型很好的解決了加載類統一的問題,類加載都是由子加載器向上委派給父加載器加載,這樣加載的類具備層次,但若是在父加載器加載的類中又要調用子加載器加載的類怎麼辦呢?
好比兩個加載器L1,L2(L1 extends L2),L2加載了類A,類A中使用了類B(類B在L1搜索範圍內,應由L1加載),則L2做爲類B的初始加載器並向上委託父加載器加載,最終,父加載器加載失敗,L2嘗試本身加載。能夠想象,L2在本身搜索範圍也找不到類B,最終加載失敗。
要解決這個問題就要適當打破雙親委派模型的限制:
線程上下文加載器, 最典型的應用場景就是SPI技術,像JDBC,JNDI,JAXP等,接口規範都是由java核心類庫來定義的,而規範的具體實現則是由不一樣廠商提供的,要在類庫代碼中調用用戶代碼時,就需經過線程上下文加載器來完成了。
能夠經過Thread對象的setContextClassLoader
方法設置當前線程上下文加載器,若是沒有設置,則從父線程繼承,若是父線程也沒有設置過,那麼就取應用類加載器(AppClassLoader)做爲線程上下文加載器。
//ServiceLoader.java
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
複製代碼
tomcat採用不一樣的類加載機制,主要爲了解決兩個問題:
瞭解了目的,再來看看tomcat採起了那種措施:
本文不打算介紹源碼,之後我會寫一個tomcat源碼系列
注:不一樣的jvm實現不太相同,這裏的Bootstrap泛指hotspot中的Bootstrap和ExtClassloader
這是tomcat6以前的架構,common,Server(Catalina),Shared分別加載tomcat /common,/server, /shared 下的類,不過如今的版本(tomcat9)中若是配置了server.loader,shared.loader依然適用。
若是沒有配置,tomcat依然建立commonLoader,catalinaLoader,sharedLoader三個類加載器(都是common類加載器實例,加載/lib目錄下的類),因此通常架構以下:
如今再來看:
/WEB-INF/classes
, /WEB-INF/lib/*
下的類,應用級別隔離問題完美解決,WebappX加載順序:
能夠看到/class, /lib目錄下類加載優先級高於系統類加載器和common類加載器。
能夠配置<Loader delegate="true"/>
強行讓其按雙親委派模型加載
原文地址:原文
往期內容:
歡迎關注!