ClassLoader介紹
類加載器是負(fù)責(zé)加載類的一個(gè)對(duì)象,ClassLoader是一個(gè)抽象類田柔。最常見(jiàn)的加載策略是根據(jù)的類的全名俐巴,然后找到這個(gè)類的class文件,然后從文件讀取這個(gè)類的數(shù)據(jù)加載到JVM硬爆。每個(gè)類都能通過(guò)getClassLoader方法獲取加載這個(gè)類的類加載器欣舵。
數(shù)組類的類對(duì)象不是由類加載器創(chuàng)建的,而是根據(jù)Java運(yùn)行時(shí)的需要自動(dòng)創(chuàng)建的缀磕。 Class#getClassLoader()返回的數(shù)組類的類加載器與其元素類型的類加載器相同;如果元素類型是基本類型缘圈,則數(shù)組類沒(méi)有類加載器。
應(yīng)用程序可以繼承 ClassLoader來(lái)自定義自己的累加器 袜蚕,以便擴(kuò)展Java虛擬機(jī)動(dòng)態(tài)加載類的方式糟把。
類使用雙親委派模型來(lái)加載類,ClassLoader的每個(gè)實(shí)例都有一個(gè)父類加載器牲剃。當(dāng)一個(gè)類加載加載類時(shí)遣疯,它會(huì)把這個(gè)類加載請(qǐng)求委派給它的父類加載器來(lái)加載。JVM的內(nèi)置類加載器是"bootstrap class loader"”凿傅,它的parent為null缠犀,但可以作為ClassLoader實(shí)例的父級(jí)。
通常狭归,Java虛擬機(jī)以與平臺(tái)相關(guān)的方式從本地文件系統(tǒng)加載類夭坪。例如,在UNIX系統(tǒng)上过椎,虛擬機(jī)從CLASSPATH環(huán)境變量定義的目錄中加載類室梅。 但是,某些類可能不是來(lái)自文件疚宇,它們可能來(lái)自其他來(lái)源亡鼠,例如網(wǎng)絡(luò),或者它們可以由應(yīng)用程序構(gòu)建敷待。方法defineClass(String间涵,byte [],int榜揖,int)將字節(jié)數(shù)組轉(zhuǎn)換為類Class的實(shí)例勾哩,也可以使用 Class#newInstance創(chuàng)建此新定義的類的實(shí)例抗蠢。
例如,應(yīng)用程序可以創(chuàng)建網(wǎng)絡(luò)類加載器以從服務(wù)器下載類文件思劳。示例代碼可能如下所示:
ClassLoader loader = new NetworkClassLoader(host,port);
Object main = loader.loadClass("Main", true).newInstance();
NetworkClassLoader必須實(shí)現(xiàn) findClass方法來(lái)加載類,以及自定義加載動(dòng)作通過(guò)loadClassData()方法從網(wǎng)絡(luò)加載類迅矛。一旦下載了構(gòu)成類的字節(jié),它應(yīng)該使用defineClass方法 來(lái)創(chuàng)建一個(gè)類實(shí)例潜叛。示例實(shí)現(xiàn)是:
class NetworkClassLoader extends ClassLoader {
* String host;
* int port;
*
* public Class findClass(String name) {
* byte[] b = loadClassData(name);
* return defineClass(name, b, 0, b.length);
* }
*
* private byte[] loadClassData(String name) {
* // load the class data from the connection
*
* }
* }
findClass方法參數(shù)name要符合JVM定義的規(guī)范秽褒,以下這些都是合法的類名稱:
1 "java.lang.String"
2 "javax.swing.JSpinner$DefaultEditor"
3 "java.security.KeyStore$Builder$FileBuilder$1"
4 "java.net.URLClassLoader$3$1"
何時(shí)出發(fā)類加載動(dòng)作?
類加載的觸發(fā)可以分為隱式加載和顯示加載威兜。
隱式加載
隱式加載包括以下幾種情況:
- 遇到new销斟、getstatic、putstatic椒舵、invokestatic這4條字節(jié)碼指令時(shí)
- 對(duì)類進(jìn)行反射調(diào)用時(shí)
- 當(dāng)初始化一個(gè)類時(shí)蚂踊,如果其父類還沒(méi)有初始化,優(yōu)先加載其父類并初始化
- 虛擬機(jī)啟動(dòng)時(shí)逮栅,需指定一個(gè)包含main函數(shù)的主類悴势,優(yōu)先加載并初始化這個(gè)主類
顯示加載
顯示加載包含以下幾種情況:
- 通過(guò)ClassLoader的loadClass方法
- 通過(guò)Class.forName
- 通過(guò)ClassLoader的findClass方法
被加載的類存放在哪里?
JDK8之前會(huì)加載到內(nèi)存中的方法區(qū)措伐。
從JDK8到現(xiàn)在為止特纤,會(huì)加載到元數(shù)據(jù)區(qū)。
都有哪些ClassLoader侥加?
整個(gè)JVM平臺(tái)提供三類ClassLoader捧存。
Bootstrap ClassLoader
加載JVM自身工作需要的類,它由JVM自己實(shí)現(xiàn)担败。它會(huì)加載JAVA_HOME/jre/lib下的文件
ExtClassLoader
它是JVM的一部分昔穴,由sun.misc.Launcher.ExtClassLoader實(shí)現(xiàn),他會(huì)加載JAVA_HOME/jre/lib/ext目錄中的文件(或由System.getProperty("java.ext.dirs")所指定的文件)提前。
AppClassLoader
應(yīng)用類加載器吗货,我們工作中接觸最多的也是這個(gè)類加載器,它由sun.misc.Launcher.AppClassLoader實(shí)現(xiàn)狈网。它加載由System.getProperty("java.class.path")指定目錄下的文件宙搬,也就是我們通常說(shuō)的classpath路徑。
雙親委派模型
雙親委派模型原理
從JDK1.2之后拓哺,類加載器引入了雙親委派模型勇垛,其模型圖如下:
其中,兩個(gè)用戶自定義類加載器的父加載器是AppClassLoader士鸥,AppClassLoader的父加載器是ExtClassLoader闲孤,ExtClassLoader是沒(méi)有父類加載器的,在代碼中烤礁,ExtClassLoader的父類加載器為null讼积。BootstrapClassLoader也并沒(méi)有子類肥照,因?yàn)樗耆蒍VM實(shí)現(xiàn)。
雙親委派模型的原理是:當(dāng)一個(gè)類加載器接收到類加載請(qǐng)求時(shí)勤众,首先會(huì)請(qǐng)求其父類加載器加載建峭,每一層都是如此,當(dāng)父類加載器無(wú)法找到這個(gè)類時(shí)(根據(jù)類的全限定名稱)决摧,子類加載器才會(huì)嘗試自己去加載。
此模型解決的問(wèn)題
為什么要使用雙親委派模型呢凑兰?它可以解決什么問(wèn)題呢掌桩?
親委派模型是JDK1.2之后引入的。根據(jù)雙親委派模型原理姑食,可以試想波岛,沒(méi)有雙親委派模型時(shí),如果用戶自己寫(xiě)了一個(gè)全限定名為java.lang.Object的類音半,并用自己的類加載器去加載则拷,同時(shí)BootstrapClassLoader加載了rt.jar包中的JDK本身的java.lang.Object,這樣內(nèi)存中就存在兩份Object類了曹鸠,此時(shí)就會(huì)出現(xiàn)很多問(wèn)題煌茬,例如根據(jù)全限定名無(wú)法定位到具體的類。
有了雙親委派模型后彻桃,所有的類加載操作都會(huì)優(yōu)先委派給父類加載器坛善,這樣一來(lái),即使用戶自定義了一個(gè)java.lang.Object邻眷,但由于BootstrapClassLoader已經(jīng)檢測(cè)到自己加載了這個(gè)類眠屎,用戶自定義的類加載器就不會(huì)再重復(fù)加載了。
所以肆饶,雙親委派模型能夠保證類在內(nèi)存中的唯一性改衩。
雙親委派模型實(shí)現(xiàn)原理
下面從源碼的角度看一下雙親委派模型的實(shí)現(xiàn)。
JVM在加載一個(gè)class時(shí)會(huì)先調(diào)用classloader的loadClassInternal方法驯镊,該方法源碼如下
// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
throws ClassNotFoundException
{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
該方法里面做的事兒就是調(diào)用了loadClass方法葫督,loadClass方法的實(shí)現(xiàn)如下
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 先查看這個(gè)類是否已經(jīng)被自己加載了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果有父類加載器,先委派給父類加載器來(lái)加載
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父類加載器為null阿宅,說(shuō)明ExtClassLoader也沒(méi)有找到目標(biāo)類候衍,則調(diào)用BootstrapClassLoader來(lái)查找
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果都沒(méi)有找到,調(diào)用findClass方法洒放,嘗試自己加載這個(gè)類
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īng)給出了幾個(gè)關(guān)鍵步驟的說(shuō)明蛉鹿。
代碼中調(diào)用BootstrapClassLoader的地方實(shí)際是調(diào)用的native方法。
由此可見(jiàn)往湿,雙親委派模型實(shí)現(xiàn)的核心就是這個(gè)loadClass方法妖异。
實(shí)現(xiàn)自己的類加載器
類加載器的作用
類加載器有啥作用呢惋戏?我們?cè)倩氐缴厦娴脑创a。
從上文我們知道JVM通過(guò)loadClass方法來(lái)查找類他膳,所以响逢,他的第一個(gè)作用也是最重要的:在指定的路徑下查找class文件(各個(gè)類加載器的掃描路徑在上文已經(jīng)給出)。
然后棕孙,當(dāng)父類加載器都說(shuō)沒(méi)有加載過(guò)目標(biāo)類時(shí)舔亭,他會(huì)嘗試自己加載目標(biāo)類,這就調(diào)用了findClass方法蟀俊,可以看一下findClass方法的定義:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
可以發(fā)現(xiàn)他要求返回一個(gè)Class對(duì)象實(shí)例钦铺,這里我通過(guò)一個(gè)實(shí)現(xiàn)類sun.rmi.rmic.iiop.ClassPathLoader來(lái)說(shuō)明一下findClass都干了什么。
protected Class findClass(String var1) throws ClassNotFoundException {
// 從指定路徑加載指定名稱的class的字節(jié)流
byte[] var2 = this.loadClassData(var1);
// 通過(guò)ClassLoader的defineClass來(lái)創(chuàng)建class對(duì)象實(shí)例
return this.defineClass(var1, var2, 0, var2.length);
}
他做的事情在注釋中已經(jīng)給出肢预,可以看到矛洞,最終是通過(guò)defineClass方法來(lái)實(shí)例化class對(duì)象的。
另外可以發(fā)現(xiàn)烫映,class文件字節(jié)的獲取和處理我們是可以控制的沼本。所以,第二個(gè)作用:我們可以在字節(jié)流解析這一步做一些自定義的處理锭沟。 例如抽兆,加解密。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}
被final掉了冈钦,沒(méi)辦法覆寫(xiě)郊丛,所以這里看似不能做什么事兒了。
小結(jié)一下:
- 通過(guò)loadClass在指定的路徑下查找文件瞧筛。
- 通過(guò)findClass方法解析class字節(jié)流厉熟,并實(shí)例化class對(duì)象。
什么時(shí)候需要自己實(shí)現(xiàn)類加載器
當(dāng)JDK提供的類加載器實(shí)現(xiàn)無(wú)法滿足我們的需求時(shí)较幌,才需要自己實(shí)現(xiàn)類加載器揍瑟。
根據(jù)上述類加載器的作用,可能有以下幾個(gè)場(chǎng)景需要自己實(shí)現(xiàn)類加載器
- 當(dāng)需要在自定義的目錄中查找class文件時(shí)(或網(wǎng)絡(luò)獲日)
- class被類加載器加載前的加解密(代碼加密領(lǐng)域)
如何實(shí)現(xiàn)自己的類加載器
接下來(lái)绢片,實(shí)現(xiàn)一個(gè)在自定義class類路徑中查找并加載class的自定義類加載器。
package com.lordx.sprintbootdemo.classloader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
/**
* 自定義ClassLoader
* 功能:可自定義class文件的掃描路徑
* @author zhiminxu
*/
// 繼承ClassLoader岛琼,獲取基礎(chǔ)功能
public class TestClassLoader extends ClassLoader {
// 自定義的class掃描路徑
private String classPath;
public TestClassLoader(String classPath) {
this.classPath = classPath;
}
// 覆寫(xiě)ClassLoader的findClass方法
protected Class<?> findClass(String name) throws ClassNotFoundException {
// getDate方法會(huì)根據(jù)自定義的路徑掃描class底循,并返回class的字節(jié)
byte[] classData = getDate(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
// 生成class實(shí)例
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getDate(String name) {
// 拼接目標(biāo)class文件路徑
String path = classPath + File.separatorChar + name.replace('.', File.separatorChar) + ".class";
try {
InputStream is = new FileInputStream(path);
ByteArrayOutputStream stream = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int num = 0;
while ((num = is.read(buffer)) != -1) {
stream.write(buffer, 0 ,num);
}
return stream.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
使用自定義的類加載器
package com.lordx.sprintbootdemo.classloader;
public class MyClassLoader {
public static void main(String[] args) throws ClassNotFoundException {
// 自定義class類路徑
String classPath = "/Users/zhiminxu/developer/classloader";
// 自定義的類加載器實(shí)現(xiàn):TestClassLoader
TestClassLoader testClassLoader = new TestClassLoader(classPath);
// 通過(guò)自定義類加載器加載
Class<?> object = testClassLoader.loadClass("ClassLoaderTest");
// 這里的打印應(yīng)該是我們自定義的類加載器:TestClassLoader
System.out.println(object.getClassLoader());
}
}
跟ClassLoader相關(guān)的幾個(gè)異常
ClassNotFoundException
這個(gè)異常,相信大家經(jīng)常遇到槐瑞。
那么熙涤,到底啥原因?qū)е聮伋鲞@個(gè)異常呢?
看一下ClassLoader的源碼,在JVM調(diào)用loadClassInternal的方法中祠挫,就會(huì)拋出這個(gè)異常那槽。
其聲明如下:
// This method is invoked by the virtual machine to load a class.
private Class<?> loadClassInternal(String name)
throws ClassNotFoundException
{
// For backward compatibility, explicitly lock on 'this' when
// the current class loader is not parallel capable.
if (parallelLockMap == null) {
synchronized (this) {
return loadClass(name);
}
} else {
return loadClass(name);
}
}
這里面loadClass方法會(huì)拋出這個(gè)異常,再來(lái)看loadClass方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
調(diào)用了重寫(xiě)的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 查看findLoadedClass的聲明等舔,沒(méi)有拋出這個(gè)異常
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 這里會(huì)拋骚灸,但同樣是調(diào)loadClass方法,無(wú)需關(guān)注
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// catch住沒(méi)有拋慌植,因?yàn)橐谙旅鎳L試自己獲取class
// 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();
// 關(guān)鍵:這里會(huì)拋
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;
}
}
再來(lái)看findClass方法
/**
* Finds the class with the specified <a href="#name">binary name</a>.
* This method should be overridden by class loader implementations that
* follow the delegation model for loading classes, and will be invoked by
* the {@link #loadClass <tt>loadClass</tt>} method after checking the
* parent class loader for the requested class. The default implementation
* throws a <tt>ClassNotFoundException</tt>.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*
* @since 1.2
*/
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
果然這里拋出的甚牲,注釋中拋出這個(gè)異常的原因是說(shuō):當(dāng)這個(gè)class無(wú)法被找到時(shí)拋出。這也就是為什么在上面自定義的類加載器中蝶柿,覆寫(xiě)findClass方法時(shí)鳖藕,如果沒(méi)有找到class要拋出這個(gè)異常的原因。
至此只锭,這個(gè)異常拋出的原因就明確了:在雙親委派模型的所有相關(guān)類加載器中,目標(biāo)類在每個(gè)類加載器的掃描路徑中都不存在時(shí)院尔,會(huì)拋出這個(gè)異常蜻展。
NoClassDefFoundError
這也是個(gè)經(jīng)常會(huì)碰到的異常,而且不熟悉的同學(xué)可能經(jīng)常搞不清楚什么時(shí)候拋出ClassNotFoundException邀摆,什么時(shí)候拋出NoClassDefFoundError纵顾。
我們還是到ClassLoader中搜一下這個(gè)異常,可以發(fā)現(xiàn)在defineClass方法中可能拋出這個(gè)異常栋盹,defineClass方法源碼如下:
/**
* ... 忽略注釋和參數(shù)以及返回值的說(shuō)明施逾,直接看異常聲明
*
* @throws ClassFormatError
* If the data did not contain a valid class
*
* 在這里。由于NoClassDefFoundError是Error下的例获,所以不用顯示throws
* @throws NoClassDefFoundError
* If <tt>name</tt> is not equal to the <a href="#name">binary
* name</a> of the class specified by <tt>b</tt>
*
* @throws IndexOutOfBoundsException
* If either <tt>off</tt> or <tt>len</tt> is negative, or if
* <tt>off+len</tt> is greater than <tt>b.length</tt>.
*
* @throws SecurityException
* If an attempt is made to add this class to a package that
* contains classes that were signed by a different set of
* certificates than this class, or if <tt>name</tt> begins with
* "<tt>java.</tt>".
*/
protected final Class<?> defineClass(String name, byte[] b, int off, int len,
ProtectionDomain protectionDomain)
throws ClassFormatError
{
protectionDomain = preDefineClass(name, protectionDomain);
String source = defineClassSourceLocation(protectionDomain);
Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
postDefineClass(c, protectionDomain);
return c;
}
繼續(xù)追蹤里面的方法汉额,可以發(fā)現(xiàn)是preDefineClass方法拋出的,這個(gè)方法源碼如下:
/* Determine protection domain, and check that:
- not define java.* class,
- signer of this class matches signers for the rest of the classes in
package.
*/
private ProtectionDomain preDefineClass(String name,
ProtectionDomain pd)
{
// 這里顯示拋出榨汤,可發(fā)現(xiàn)是在目標(biāo)類名校驗(yàn)不通過(guò)時(shí)拋出的
if (!checkName(name))
throw new NoClassDefFoundError("IllegalName: " + name);
// Note: Checking logic in java.lang.invoke.MemberName.checkForTypeAlias
// relies on the fact that spoofing is impossible if a class has a name
// of the form "java.*"
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
if (pd == null) {
pd = defaultDomain;
}
if (name != null) checkCerts(name, pd.getCodeSource());
return pd;
}
這個(gè)校驗(yàn)的源碼如下
// true if the name is null or has the potential to be a valid binary name
private boolean checkName(String name) {
if ((name == null) || (name.length() == 0))
return true;
if ((name.indexOf('/') != -1)
|| (!VM.allowArraySyntax() && (name.charAt(0) == '[')))
return false;
return true;
}
所以蠕搜,這個(gè)異常拋出的原因是:類名校驗(yàn)未通過(guò)。
但這個(gè)異常其實(shí)不止在ClassLoader中拋出收壕,其他地方妓灌,例如框架中,web容器中都有可能拋出蜜宪,還要具體問(wèn)題具體分析虫埂。