本文主要包含下面幾個內(nèi)容:
- classloader雙親委派機制以及classloader加載class的流程
- classloader的其他特性
- 自定義classloader以及如何打破雙親委派機制
- context classloader作用
classloader雙親委派機制以及classloader加載class的流程
java類加載流程
JVM啟動時鸠珠,有三個classloader負(fù)責(zé)加載class,如下:
- bootstrap classloader
- extension classloader
- system classloader
- bootstrap classloader:采用native code實現(xiàn)拍柒,是JVM的一部分,主要加載JVM自身工作需要的類屈暗; 這些類位于$JAVA_HOME/jre/lib/下面斤儿。當(dāng)JVM啟動后,Bootstrap ClassLoader也隨著啟動恐锦,負(fù)責(zé)加載完核心類庫后,并構(gòu)造Extension ClassLoader和App ClassLoader類加載器疆液。
- extension classloader:擴展的class loader一铅,加載位于$JAVA_HOME/jre/lib/ext目錄下的擴展jar。
- system classloader: 系統(tǒng)class loader堕油,父類是ExtClassLoader潘飘,加載$CLASSPATH下的目錄和jar;它負(fù)責(zé)加載應(yīng)用程序主函數(shù)類掉缺。
為了更好的理解卜录,直接查看源碼,省略了非關(guān)鍵代碼眶明。sun.misc.Launcher
, 它是java程序的入口艰毒。
public class Launcher {
private static URLStreamHandlerFactory factory = new Launcher.Factory();
private static Launcher launcher = new Launcher();
private static String bootClassPath = System.getProperty("sun.boot.class.path");
private ClassLoader loader;
private static URLStreamHandler fileHandler;
public static Launcher getLauncher() {
return 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);
String var2 = System.getProperty("java.security.manager");
if (var2 != null) {
SecurityManager var3 = null;
if (!"".equals(var2) && !"default".equals(var2)) {
try {
var3 = (SecurityManager)this.loader.loadClass(var2).newInstance();
} catch (IllegalAccessException var5) {
} catch (InstantiationException var6) {
} catch (ClassNotFoundException var7) {
} catch (ClassCastException var8) {
}
} else {
var3 = new SecurityManager();
}
if (var3 == null) {
throw new InternalError("Could not create SecurityManager: " + var2);
}
System.setSecurityManager(var3);
}
}
static class ExtClassLoader extends URLClassLoader {
//...
}
static class AppClassLoader extends URLClassLoader {
//...
}
//...
}
bootstrap classloader負(fù)責(zé)加載Launcher類,其中代碼里面的bootClassPath為bootstrap classloader的加載路徑搜囱,獲取sun.boot.class.path屬性為$JAVA_HOME/jre/lib/下面jar拼接成的丑瞧,如下:
D:\java_tools\java\jdk8\jre\lib\resources.jar
D:\java_tools\java\jdk8\jre\lib\rt.jar
D:\java_tools\java\jdk8\jre\lib\sunrsasign.jar
D:\java_tools\java\jdk8\jre\lib\jsse.jar
D:\java_tools\java\jdk8\jre\lib\jce.jar
D:\java_tools\java\jdk8\jre\lib\charsets.jar
D:\java_tools\java\jdk8\jre\lib\jfr.jar
D:\java_tools\java\jdk8\jre\classes
同時在代碼里面構(gòu)造了ExtClassLoader和AppClassLoader,兩者都繼承了URLClassLoader蜀肘,其中ExtClassLoader的parent為null(其中為null表示parent為bootstrap classloader)绊汹,URLs為System.getProperty("java.ext.dirs")
, 值為$JAVA_HOME/jre/lib/ext,具體的代碼在var1 = Launcher.ExtClassLoader.getExtClassLoader();
另外AppClassLoader的parent為ExtClassLoader扮宠,URLs為System.getProperty("java.class.path")
獲取的值西乖,值為-classpath傳遞的值, 具體的代碼在this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
所以classloader的繼承關(guān)系如下:
+-BootstrapClassLoader [bootstrap classloader]
+-sun.misc.Launcher$ExtClassLoader@7bf2dede [extension classloader]
+-sun.misc.Launcher$AppClassLoader@18b4aac2 [system classloader]
雙親委派機制
ExtClassLoader和AppClassLoader都繼承了URLClassLoader, URLClassLoader又繼承了ClassLoader類,類加載器在加載類的時候坛增,最終會調(diào)用ClassLoader類的loadClass方法获雕,正是該方法決定了類的加載機制是雙親委派,源碼如下:
public abstract class ClassLoader {
//...
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;
}
}
//...
}
可以看到主要分為幾步:
- 根據(jù)類名嘗試從本地緩存里面獲取已經(jīng)加載的class收捣,如果沒有轉(zhuǎn)2典鸡,如果有轉(zhuǎn)最后一步。
- 判斷parent是否為null:不為null坏晦,直接使用parent的classloader加載萝玷;為null嫁乘,相當(dāng)于parent是bootstrap classloader,使用bootstrap classloader加載球碉。如果沒有轉(zhuǎn)3蜓斧,如果有轉(zhuǎn)最后一步。
- 調(diào)用
findClass
方法睁冬,根據(jù)一定的路徑策略獲取class挎春,沒有找到的話返回null,找到轉(zhuǎn)最后一步豆拨。 - 解析class直奋。
上面的幾個步驟可以看到,優(yōu)先由parent的classloader加載施禾,這就是雙親委派機制脚线。其中第3步可以覆蓋 findClass方法,實現(xiàn)自己的加載策略:比如可以從遠(yuǎn)程網(wǎng)絡(luò)class文件弥搞,從本地壓縮包里面獲取class文件等邮绿。
classloader的其他特性
除了雙親委派特性,classloader還有隱式加載攀例,隔離等特性船逮。
隱式加載
JVM加載class文件到內(nèi)存有兩種方式。
- 隱式加載:所謂隱式加載就是不通過在代碼里調(diào)用classloader來加載需要的類粤铭,而是通過JVM來自動加載需要的類到內(nèi)存的方式挖胃。例如,當(dāng)我們在類中繼承或者引用某個類時梆惯,JVM在解析當(dāng)前這個類時發(fā)現(xiàn)引用的類不在內(nèi)存中冠骄,那么就會自動將這些類加載到內(nèi)存中。
- 顯式加載:相反的顯式加載就是我們在代碼中通過調(diào)用classloader類來加載一個類的方式加袋,調(diào)用
this.getClass.getClassLoader().loadClass()
或者Class.forName()
凛辣,或者我們自己實現(xiàn)的ClassLoader的findClass()
方法等。
其實這兩種方式是混合使用的职烧,例如扁誓,我們通過自定義的classloader顯式加載一個類時,這個類中又引用了其他類蚀之,那么這些類就是隱式加載的蝗敢。正如所有程序都有一個main函數(shù)一樣,所有的應(yīng)用都有一個或多個入口的類足删,這個類是被最先加載的寿谴,并且隨后的所有類都像樹枝一樣以此類為根被加載。
舉兩個例子:
- java程序運行的時候失受,都會首先從擁有main方法的入口類運行讶泰,該類由AppClassLoader加載咏瑟,從而其他被它應(yīng)用的類都會由AppClassLoader來加載。
- springboot應(yīng)用的啟動方式痪署,基于springboot的應(yīng)用码泞,最終會被打成一個jar包的形式運行,jar包的META-INF/MANIFEST.MF文件里面指定Main-Class以及其他相關(guān)關(guān)鍵信息如下:
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.dada.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Main-Class為org.springframework.boot.loader.JarLauncher
狼犯, jar包啟動的時候會首先執(zhí)行JarLauncher的main方法余寥。大致的邏輯是:在JarLauncher里面使用自定義的LaunchedURLClassLoader
(parent為system ClassLoader)加載真實的Main-Class,對應(yīng)上面的Start-Class悯森,關(guān)鍵源碼如下:
package org.springframework.boot.loader;
import java.lang.reflect.*;
public class MainMethodRunner
{
// 省略非關(guān)鍵代碼
public void run() throws Exception {
// this.mainClassName 對應(yīng)的就是上面META-INF/MANIFEST.MF里面的Start-Class屬性
final Class<?> mainClass = Thread.currentThread().getContextClassLoader().loadClass(this.mainClassName);
final Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
mainMethod.invoke(null, this.args);
}
}
其中Thread.currentThread().getContextClassLoader()
獲取的就是LaunchedURLClassLoader
(在前面設(shè)置宋舷,具體可以參考org.springframework.boot.loader.Launcher#launch
方法),通過顯示加載的方式加載Start-Class:com.dada.Application
(即真正的應(yīng)用Main-Class)瓢姻,也就是應(yīng)用的入口類祝蝠,該入口類會讓其他被它引用的類使用LaunchedURLClassLoader
進行加載。
隔離性
為了理解隔離性汹来,需要先理解下面幾個概念
- 不同的classloader加載的同一個class文件,會被jvm認(rèn)為是不同的class改艇。如果把一個ClassLoader創(chuàng)建的實例收班,賦值給另一個ClassLoader加載的類,會導(dǎo)致ClassCastException異常谒兄。
- class沖突摔桦,同一個classloader只能加載一個class name(包括package)的class,如果存在多個class name相同的類承疲,會出現(xiàn)隨機加載class邻耕,從而導(dǎo)致NoSuchMethodError等異常。
- 兩個平級的classloader加載的兩個類燕鸽,不能相互訪問兄世,比如在下面的場景:
+-BootstrapClassLoader [bootstrap classloader]
+-sun.misc.Launcher$ExtClassLoader@7bf2dede [extension classloader]
+-sun.misc.Launcher$AppClassLoader@18b4aac2 [system classloader]
+-自定義的classloader1
+-自定義的classloader2
其中classloader1加載的class不能訪問classloader2加載的class。
- 一個classloader可以訪問父classloader加載的class啊研,比如自定義的classloader1可以訪問AppClassLoader加載的類御滩。這是雙親委派機制決定的。
- 父classloader加載的class不能訪問子classloader加載的class党远,比如AppClassLoader不能訪問自定義的classloader1加載的類削解。這也是雙親委派機制決定的。
舉個例子:
一個tomcat可以同時啟動多個不同的webapp(基于springmvc)沟娱,多個不同的webapp可能擁有完全相同的類氛驮,那么是如何保證不會出現(xiàn)class沖突?正是使用了不同classloader的隔離特性济似。每個webapp使用自定義的WebappClassLoader
(parent為shared classloader)來加載org.springframework.web.servlet.DispatcherServlet
(繼承了Servlet接口)矫废,這邊的DispatcherServlet類相當(dāng)于入口類盏缤,根據(jù)上面的隱式加載,會繼續(xù)使用該classloader加載相關(guān)聯(lián)的類磷脯。每個webappClassLoader是同級關(guān)系蛾找,不會存在相互訪問的問題,從而達(dá)到不同webapp應(yīng)用隔離的目的赵誓。
自定義classloader以及如何打破雙親委派機制
正是因為classloader有著上面的特性:雙親委派打毛,隱式加載,隔離性俩功,所以經(jīng)常會有自定義classloader的需求幻枉。
自定義classloader之后,可以與原有classloader加載的類隔離開來诡蜓,從而可以避免對原有classloader加載的類造成干擾熬甫。同時可以覆蓋loadClass方法和findClass方法,打破雙親委派機制蔓罚,實現(xiàn)自定義的class路徑加載椿肩。下面舉幾個例子:
- OSGI不同bundle之間的隔離
OSGI是Java動態(tài)化模塊化系統(tǒng),會有多個部署單元豺谈,每個部署單元稱為一個bundle郑象。每個bundle有自己獨立的classloader,同時一個bundle又可以使用其他bundle導(dǎo)出的package茬末,相當(dāng)于委托另外一個bundle的classloader進行類的加載厂榛。 - 螞蟻金服開源的sofa-ark框架
sofa-ark是一款基于Java實現(xiàn)的輕量級類隔離加載容器,sofa-ark包含三個概念:
sofa-ark模板圖
ark plugin和ark biz都是以jar包的形式存在丽惭,其中每個ark plugin使用自定義的PluginClassLoader
來加載击奶,每個ark biz也使用自定義的BizClassLoader
來加載。這樣可以使不同的ark plugin和不同的ark biz隔離開來责掏,以PluginClassLoader
為例:
public class PluginClassLoader extends AbstractClasspathClassloader {
...
@Override
protected Class<?> loadClassInternal(String name, boolean resolve) throws ArkLoaderException {
// 1. sun reflect related class throw exception directly
if (classloaderService.isSunReflectClass(name)) {
throw new ArkLoaderException(
String
.format(
"[ArkPlugin Loader] %s : can not load class: %s, this class can only be loaded by sun.reflect.DelegatingClassLoader",
pluginName, name));
}
// 2. findLoadedClass
Class<?> clazz = findLoadedClass(name);
// 3. JDK related class
if (clazz == null) {
clazz = resolveJDKClass(name);
}
// 4. Ark Spi class
if (clazz == null) {
clazz = resolveArkClass(name);
}
// 5. Import class export by other plugins
if (clazz == null) {
clazz = resolveExportClass(name);
}
// 6. Plugin classpath class
if (clazz == null) {
clazz = resolveLocalClass(name);
}
// 7. Java Agent ClassLoader for agent problem
if (clazz == null) {
clazz = resolveJavaAgentClass(name);
}
if (clazz != null) {
if (resolve) {
super.resolveClass(clazz);
}
return clazz;
}
throw new ArkLoaderException(String.format(
"[ArkPlugin Loader] %s : can not load class: %s", pluginName, name));
}
...
}
loadClass
方法會調(diào)用loadClassInternal
方法柜砾,當(dāng)Plugin在運行時發(fā)現(xiàn)一個類需要被加載時,會按照如下步驟搜索:
- 如果已加載過换衬,那就返回已加載好的那個類局义。
- 如果這個類是JDK自己的,那么就用
JDKClassLoader
去加載冗疮。 - 如果這個類是屬于Ark容器的萄唇,那么就用
ArkClassLoader
去加載。 - 如果這個類是某個插件export的术幔,那么就用
ExportClassLoader
去加載另萤。 - 如果這個類是插件自身的,那么就用當(dāng)前的ClassLoader直接loadClass就好。
- 最后使用某個java agent嘗試加載四敞。
- 實在找不到就報錯泛源。
可以看到該步驟并沒有使用雙親委派的機制,而是自定義的加載策略忿危。
context classloader作用
context classloader概念
經(jīng)常會在代碼里面看到這樣的代碼:
ClassLoader cl= Thread.currentThread().getContextClassLoader();
Class<?> clazz= cl.loadClass(getClassName());
每一個Thread都有一個相關(guān)聯(lián)的context classloader达箍,可以通過Thread.setContextClassLoader()
方法設(shè)置。如果沒有主動設(shè)置铺厨,Thread默認(rèn)繼承Parent Thread的 context classloader缎玫。如果你整個應(yīng)用中都沒有對此作任何處理,那么 所有的Thread都會以system classLoader作為context Classloader解滓。
context classloader場景
可以自定義classloader赃磨,并設(shè)置到線程中,這樣在當(dāng)前線程的任何地方都可以使用該classloader進行顯示加載洼裤,即調(diào)用loadClass或者forName方法邻辉。從而可以靈活的使用自定義的classloader,而不被java自帶的classloader所限制腮鞍。
最常見的是在Java的SPI場景中使用值骇,比如JDBC。這些 SPI 的接口由 Java 核心庫來提供移国,而這些 SPI 的實現(xiàn)代碼則是作為 Java 應(yīng)用所依賴的 jar 包被包含進classpath里面吱瘩。SPI的接口是Java核心庫的一部分,是由bootstrap classloader來加載的桥狡,SPI的實現(xiàn)類一般由system classloader來加載搅裙。
以JDBC為例皱卓,直接看源碼裹芝,省略了非關(guān)鍵代碼:
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
...
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
...
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
...
}
DriverManager類是JDK核心類,會被bootstrap classloader加載娜汁,在加載的時候會調(diào)用static代碼塊加載JDBC驅(qū)動嫂易,在loadInitialDrivers
方法里面調(diào)用ServiceLoader.load(Driver.class)
,ServiceLoader是SPI的是一種實現(xiàn)掐禁,所謂SPI怜械,即Service Provider Interface,用于一些服務(wù)提供給第三方實現(xiàn)或者擴展傅事,可以增強框架的擴展或者替換一些組件缕允。 繼續(xù)看load方法。
public final class ServiceLoader<S>, implements Iterable<S> {
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
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();
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
}
可以看到會將context classloader傳遞給ServiceLoader
蹭越,并最終賦值給loader屬性障本,在調(diào)用driversIterator.next()遍歷時,最終會調(diào)用nextService方法,可以看到nextService方法里面調(diào)用Class.forName(cn, false, loader)
進行類的隱式加載驾霜。其中cn為所有通過spi方式注冊的driver案训,比如mysql驅(qū)動的類名為com.mysql.jdbc.Driver,配置在mysql-connector-java-5.1.41.jar里的META-INF/services/java.sql.Driver文件中:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
通過這種方式保證了bootstrap classloader加載的DriverManager類可以訪問由system classloader加載的具體SPI實現(xiàn)類com.mysql.jdbc.Driver粪糙。
參考文檔
理解Java ClassLoader機制
springboot應(yīng)用啟動原理(二) 擴展URLClassLoader實現(xiàn)嵌套jar加載
淺議tomcat與classloader
通過tomcat源碼查看其如何實現(xiàn)應(yīng)用相互隔離
sofa-ark官方文檔
真正理解ContextClassLoader