1. 概述
類加載器實際定義了類的namespace。
package java.lang;
public abstract class ClassLoader {
public Class loadClass(String name);
protected Class defineClass(byte[] b);
public URL getResource(String name);
public Enumeration getResources(String name);
public ClassLoader getParent();//
}
2.類加載方式之當前類加載器和指定類加載器
類的加載只有兩種加載方式胶惰,即當前類加載器加載(JVM自動行為本慕,無法干預)和指定類加載器加載(自己指定類加載器進行加載)踏堡。
2.1 當前類加載器
class A{
B b;
}
B的加載會使用A的類的類加載器進行加載碌燕,A的類加載器就是當前類加載器。這種類加載方式是JVM自動進行的倔幼,無法干預盖腿。
2.2 指定類加載器
代碼指定類加載器進行加載
3. 類加載器分類:定義類加載器和初始類加載器
-
定義類加載器
類的真實加載器,即通過class.getClassLoader()獲得的類加載器。 -
初始類加載器
類最先是由初始類加載器進行加載翩腐,初始類加載器并不一定是真正最后加載到類的加載器鸟款。
4. 各種類加載器
-
BootStarpClassLoader
Bootstrp加載器是用C++語言寫的,它是在Java虛擬機啟動后初始化的茂卦,它主要負責加載%JAVA_HOME%/jre/lib,-Xbootclasspath參數(shù)指定的路徑以及%JAVA_HOME%/jre/classes中的類何什。 -
ExtClassLoader
Bootstrp loader加載ExtClassLoader,并且將ExtClassLoader的父加載器設置為Bootstrp loader.ExtClassLoader是用Java寫的,具體來說就是 sun.misc.Launcher$ExtClassLoader等龙,ExtClassLoader主要加載%JAVA_HOME%/jre/lib/ext处渣,此路徑下的所有classes目錄以及java.ext.dirs系統(tǒng)變量指定的路徑中類庫。 -
AppClassLoader
系統(tǒng)類加載器而咆,加載classpath下的類庫霍比。該類是Launcher類下的內(nèi)部類(包訪問權限),所以無法new 一個系統(tǒng)類加載器
通過ClassLoader.getSystemClassLoader()
能獲得該加載器暴备,該加載器是單例的。
類加載器是有父類加載器的们豌,默認的父類加載器是系統(tǒng)類加載器 -
線程上下文類加載器
Main線程的上下文類加載器是系統(tǒng)類加載器涯捻,線程的默認上下文類加載器是父線程的上下文類加載器。
-
得到上下文類加載器
public ClassLoader getContextClassLoader()
-
設置上下文類加載器
public void setContextClassLoader(ClassLoader cl)
4.1 框架中一般怎么用上下文類記載器
試想: 如果一個JNDI的提供方望迎,或者JAXP的提供方障癌,他們的SPI是通過bootstrap加載的,但是他們的實現(xiàn)類必須通過應用ClassLoader甚至是更下層的ClassLoader來加載辩尊。那么在其初始化的過程中涛浙,需要考慮如果獲取到部署了SPI實現(xiàn)的ClassLoader,而給出的方案是使用ContextClassLoader摄欲。比如轿亮。 在javax.xml.parsers.DocumentBuilderFactory中。
5. ClassLoader.loadClass
雙親制:
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;
}
}
6. Class.forName和ClassLoader.loadClass過程
6.1 Class.forName
這里討論的
Class.forName
是public static Class<?> forName(String className) throws ClassNotFoundException
這里討論的ClassLoader.loadClass
是public Class<?> loadClass(String name) throws ClassNotFoundException
Class.forName是根據(jù)給定的類型全名從當前類加載器中加載指定的類型胸墙。
實現(xiàn)代碼:
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
Reflection.getCallerClass()
我注,獲取到調(diào)用Class.forName方法的類,隱含意義就是當前類加載器迟隅。加載的邏輯在native方法forName0中定義但骨,也就是forName進行的類加載行為已經(jīng)脫離了Java代碼的控制范圍,進入到了Java運行時環(huán)境把控的階段智袭。
以下是JDK實現(xiàn)的部分代碼:
Class.c中對應的實現(xiàn)邏輯:
JNIEXPORT jclass JNICALL
Java_java_lang_Class_forName0(JNIEnv *env, jclass this, jstring classname,
jboolean initialize, jobject loader)
{
// 略
cls = JVM_FindClassFromClassLoader(env, clname, initialize,
loader, JNI_FALSE);
done:
if (clname != buf) {
free(clname);
}
return cls;
}
實現(xiàn)細節(jié)在JVM_FindClassFromClassLoader中定義奔缠,可以看到調(diào)用Class.forName會使用JVM_FindClassFromClassLoader這個函數(shù)來進行類型加載,我們需要注意的是clname和loader這兩個變量吼野,一個是類的全限定名校哎,另一個是ClassLoader,而Class.forName所使用的ClassLoader是當前類加載器箫锤。
在jvm.cpp中FindClassFromClassLoader的對應實現(xiàn)是:
jclass find_class_from_class_loader(JNIEnv* env, Symbol* name, jboolean init, Handle loader, Handle protection_domain, jboolean throwError, TRAPS) {
// Security Note:
// The Java level wrapper will perform the necessary security check allowing
// us to pass the NULL as the initiating class loader.
klassOop klass = SystemDictionary::resolve_or_fail(name, loader, protection_domain, throwError != 0, CHECK_NULL);
// 略
}
SystemDictionary贬蛙,系統(tǒng)字典雨女,這個數(shù)據(jù)結構是保存Java加載類型的數(shù)據(jù)結構,如下圖所示阳准。
上圖黑色邊框中的內(nèi)容就是SystemDictionary氛堕,它是以類的全限定名再加上類加載器作為key,進而確定Class引用野蝇。
當在代碼中調(diào)用Class.forName(String name)或者由運行時Java進行類加載讼稚,比如:
public void m() {
B b = new B();
}
對類型B的加載,就是運行時Java進行的類加載绕沈。
類型加載時锐想,以ClassLoader和需要加載的類型全限定名作為參數(shù)在SystemDictionary中進行查詢,如果能夠查詢到則返回乍狐。如果無法找到赠摇,則調(diào)用loader.loadClass(className)進行加載,這一步將進入到Java代碼中浅蚪。
對于loadClass而言藕帜,基本等同于loader.defineClass(loader.getResource(file).getBytes()),它做了兩件事惜傲,
- 第一件洽故,通過資源定位到類文件,
- 第二件盗誊,將類文件的字節(jié)流數(shù)組傳遞給defineClass進行構造Class實例时甚。而defineClass將再一次派發(fā)給運行時Java進行執(zhí)行荒适。
字節(jié)流數(shù)組經(jīng)過ClassFileParser進行處理之后吻贿,生成了Class實例哑子,在返回Class實例前帐要,Java將name、loader和class的對應關系添加到SystemDictionary中赠橙,這樣在后續(xù)其他類型的加載過程中掉奄,就能夠快速找到這些類型,避免無謂的defineClass過程速兔。
一個類加載的過程,在運行時Java(JVM)和java代碼之間來回切換,有點復雜,我們畫一個簡單的圖來描述主要過程招拙,由于原有的類加載過程中還要處理并發(fā)問題,我們將這些內(nèi)容都去掉,只觀察類型加載的主要流程诉稍,如下圖所示。
- 調(diào)用Class.forName(className)方法,該方法會調(diào)用native的JVM實現(xiàn)心褐,調(diào)用前該方法會確定準備好需要加載的類名以及ClassLoader,將其傳遞給native方法
- 進入到JVM實現(xiàn)后桶至,首先會在SystemDictionary中根據(jù)類名和ClassLoader組成hash价涝,進行查詢,如果能夠命中覆山,則返回
- 如果加載到則返回
- 如果在SystemDictionary中無法命中吧享,將會調(diào)用Java代碼:ClassLoader.loadClass(類名),這一步將委派給Java代碼,讓傳遞的ClassLoader進行類型加載
- 以URLClassLoader為例,ClassLoader確定了類文件的字節(jié)流卵酪,但是該字節(jié)流如何按照規(guī)范生成Class對象幌蚊,這個過程在Java代碼中是沒有體現(xiàn)的,其實也就是要求調(diào)用ClassLoader.defineClass(byte[])進行解析類型,該方法將會再次調(diào)用native方法瘸羡,因為字節(jié)流對應Class對象的規(guī)范是定義在JVM實現(xiàn)中的
- 進入JVM實現(xiàn),調(diào)用SystemDictionary的resolve_stream方法粘昨,接受byte[]吞瞪,使用ClassFileParser進行解析
- SystemDictionary::define_instance_class
- 如果類型被加載了芍秆,將類名进统、ClassLoader和類型的實例引用添加到SystemDictionary中
- 返回
- 返回
- 從Java實現(xiàn)返回到Java代碼的defineClass眉菱,返回Class對象
- 返回給loadClass(Classname)方法
- 返回給Java實現(xiàn)的SystemDictionary迹栓,因為在resolve_class中調(diào)用的ClassLoader.loadClass。這里會做出一個判斷俭缓,如果加載Class的ClassLoader并非傳遞給resolve_class的ClassLoader克伊,那么會將類名、傳遞給resolve_class的ClassLoader以及類型的實例引用添加到SystemDictionary中
- 返回給Class.forName類型實例
上述的過程比較復雜华坦,但是簡化理解一下它所做的工作愿吹,我們將SystemDictionary記作緩存,Class.forName或者說Java默認的類型加載過程是:*
- 首先根據(jù)ClassLoader惜姐,我們稱之為initialClassLoader和類名查找緩存犁跪,如果緩存有椿息,則返回;
- 如果緩存沒有坷衍,則調(diào)用ClassLoader.loadClass(類名)寝优,加載到類型后,保存<類名枫耳,真實加載類的ClassLoader乏矾,類型引用>到緩存,這里真實加載類的ClassLoader我們可以叫做defineClassLoader迁杨;
- 返回的類型在交給Java之前钻心,將會判斷defineClassLoader是否等于initialClassLoader,如果不等铅协,則新增<類名捷沸,initialClassLoader,類型引用>到緩存警医。
- 這里區(qū)分initialClassLoader和defineClassLoader的原因在于亿胸,調(diào)用initialClassLoader的loadClass,可能最終委派給其他的ClassLoader進行了加載预皇。
6.2 ClassLoader.loadClass(String className)
我們在分析了Class.forName之后侈玄,再看ClassLoader.loadClass()就會變得簡單很多,這個ClassLoader就是一個指定類加載器吟温,而ClassLoader.loadClass()只是相當于一個簡單的方法調(diào)用序仙。
根據(jù)上圖所示,該過程開始于第4步鲁豪,沒有前3步潘悼,該過程簡單說就是:調(diào)用ClassLoader.loadClass(類名),加載到類型后爬橡,保存<類名治唤,真實加載類的ClassLoader,類型引用>到緩存糙申,這里真實加載類的ClassLoader我們可以叫做defineClassLoader宾添。也就是,調(diào)用ClassLoader.loadClass(類名)之后柜裸,并不一定會在緩存中生成一條<類名缕陕,ClassLoader,類型引用>的記錄疙挺,但是一定會生成一條<類名扛邑,真實加載類的ClassLoader,類型引用>的記錄铐然。(自己附注:實際上最少在JDK8之后loadClass也先到系統(tǒng)字典中查詢是否已創(chuàng)建)
6.3 ClassLoader.findLoadedClass(String className)
該方法是protected final修飾的方法蔬崩,也就是ClassLoader的子類可以內(nèi)部使用恶座,但是無法通過ClassLoader.findLoadedClass直接調(diào)用。
這個方法一直感覺很奇怪舱殿,從名稱上看就是查詢這個ClassLoader加載過的Class奥裸,如果加載過了,那么就返回類型實例沪袭。
7. 怎么創(chuàng)建一個類加載器
URLClassLoader基本能滿足一些個性的類加載需求湾宙,如果還不滿足,可以實現(xiàn)自己的類加載器冈绊。
- extend ClassLoader
- 覆蓋
protected Class<?> findClass(String name) throws ClassNotFoundException
在findClass中實現(xiàn)查找類字節(jié)碼的邏輯侠鳄,并調(diào)用protected final Class<?> defineClass(String name, byte[] b, int off, int len)
得到類。 - 注意設置父類加載器死宣。
默認的父類加載器是系統(tǒng)類加載器伟恶,如果設置父類加載器為null,真實的父類加載器是啟動類加載器毅该。
上面步驟的類加載器符合雙親制加載規(guī)范博秫。Override ClassLoader.loadClass實現(xiàn)了雙親制和緩存細節(jié),不建議打破眶掌。
5. Class.getResource(String path)
path不以’/'開頭時挡育,默認是從此類所在的包下取資源;
path 以’/'開頭時朴爬,則是從ClassPath根下獲燃春;
TestMain.class.getResource("/") == t.getClass().getClassLoader().getResource("")
7. 各種錯誤
遇到類加載器問題時召噩,可以嘗試使用下面的表格進行問題排查母赵。
類找不到 | 加載了不正確的類 | 多于一個類被加載 |
---|---|---|
ClassNotFoundException NoClassDefFoundError | IncompatibleClassChangeError NoSuchMethodError NoSuchFieldError IllegalAccessError | ClassCastException LinkageError |
IDE class lookup (Ctrl+Shift+T in Eclipse) 或者 find . -name "*.jar" -exec jar -tf {} ; \ | grep DateUtils 使用middelware-detector | 通過在啟動參數(shù)中加 -verbose:class ,觀察加載的類來自哪個jar包 使用middelware-detector |
7.1 ClassNotFoundException 和NoClassDefFoundError的區(qū)別
sometimes error on static initializer block can also result in NoClassDefFoundError.
7.2 LinkageError
LinkageError 需要觀察哪個類被不同的類加載器加載了具滴,在哪個方法或者調(diào)用處發(fā)生(交匯)的凹嘲,然后才能想解決方法,解決方法無外乎兩種构韵。
- 還是不同的類加載器加載施绎,但是相互不再交匯影響,這里需要針對發(fā)生問題的地方做一些改動贞绳,比如更換實現(xiàn)方式,避免出現(xiàn)上述問題致稀;
- 沖突的類需要由一個Parent類加載器進行加載冈闭。LinkageError 和ClassCastException 本質(zhì)是一樣的,加載自不同類加載器的類型抖单,在同一個類的方法或者調(diào)用中出現(xiàn)萎攒,如果有轉(zhuǎn)型操作那么就會拋 ClassCastException 遇八,如果是直接的方法調(diào)用處的參數(shù)或者返回值解析,那么就會產(chǎn)生 LinkageError 耍休。