本文和大家聊聊Java類加載器這檔子事。
什么是類加載器?
咱們先來給他下一個(gè)通俗點(diǎn)的定義:
將java字節(jié)碼(.class文件)轉(zhuǎn)換成類對象(java.lang.Class),加載到JVM內(nèi)存。
Java中有哪幾種類加載器?
類加載器可分為四種:
Bootstrap ClassLoader:由C++代碼實(shí)現(xiàn),加載sun.boot.class.path所指定的目錄,或<JAVA_HOME>\lib目錄中文件。
Ext ClassLoader:加載系統(tǒng)變量 java.ext.dirs所指定的目錄,或<JAVA_HOME>\lib\ext 目錄中的文件。
App ClassLoader:加載用戶類路徑下的文件。
Custom ClassLoader:開發(fā)者自己折騰的類加載器,按需進(jìn)行類文件加載。(例如:網(wǎng)絡(luò)類加載器,通過網(wǎng)絡(luò)加載.class文件)
類加載器如何工作?
第一步,類加載器向JVM查詢目標(biāo)對象(請求加載的對象)是否已經(jīng)加載過,若已經(jīng)加載過,則直接返回此對象;否則,進(jìn)行第二步。
第二步,獲得當(dāng)前類加載器的父類加載器,遞歸執(zhí)行這一步驟,由下往上,直至到達(dá)頂級父類加載器(即Bootstrap ClassLoader),進(jìn)行第三步。
第三步,從頂級父類加載器,由上而下,在自己的搜索范圍內(nèi)檢索目標(biāo)對象的類文件(.class),若檢索到,則讀取該文件的字節(jié)碼轉(zhuǎn)換成Class對象到JVM,返回目標(biāo)對象;否則,拋出ClassNotFoundException。
有讀者老爺說了,聽你白扯了半天,對類加載器的認(rèn)知還是不清楚,有沒有實(shí)質(zhì)性的東西?下面咱們來點(diǎn)實(shí)的。
類加載器的委派模式
從上文中類加載器的工作步驟,可以看出,遞歸獲取父類加載器,由下往上,直達(dá)頂級父類加載器,在從頂級父類加載器,自上而下,逐級在自己的搜索范圍內(nèi)檢索目標(biāo)對象類文件,轉(zhuǎn)換成Class對象(若檢索到)的這種行為,稱為類加載器的委派模式。
描述遞歸獲取父類加載器,由下往上,直達(dá)頂級父類加載器的代碼如下:
//代碼段截取自ClassLoader.loadClass(String name, boolean resolve)
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
}
描述從頂級父類加載器,自上而下,逐級在自己的搜索范圍內(nèi)檢索目標(biāo)對象類文件,轉(zhuǎn)換成Class對象的代碼如下:
//代碼段截取自ClassLoader.loadClass(String name, boolean resolve)
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);//在自己的搜索范圍內(nèi)進(jìn)行檢索,若找到,轉(zhuǎn)換成Class對象
.....................................
.....................................
}
if (resolve) {
resolveClass(c);
}
return c;
真正實(shí)現(xiàn)類加載的是defineClass方法,代碼如下:
//代碼段截取自URLClassLoader.findClass(final String name)
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res); //執(zhí)行類加載工作
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
回顧類加載器的委派模式,我們發(fā)現(xiàn),發(fā)起類加載請求的類加載器,不一定會最終執(zhí)行類的加載操作,可能其父類加載器來執(zhí)行類加載。
發(fā)起類加載請求的類加載器稱為初始類加載器;完成類加載操作的類加載器稱為定義類加載器。(為啥叫定義類加載?因名而得唄-defineClass)
有讀者老爺發(fā)問了,為什么要設(shè)計(jì)這種委派模式?
委派模式的作用
個(gè)人理解,設(shè)計(jì)這種委派模式的用意有兩點(diǎn):
第一,避免對象的重復(fù)加載
第二,保護(hù)Java基礎(chǔ)類型的行為
關(guān)于第一點(diǎn),自然不必多說。
對于第二點(diǎn),咱們設(shè)想一下,在沒有委派模式的場景中,用戶自己編寫了一個(gè)java.lang.String類,并將其放置在程式的類路徑(ClassPath)中,那系統(tǒng)中就存在了多個(gè)不同的String類,這使得Java的基礎(chǔ)類型的行為無法得到保證,程式也會變得很混亂。
細(xì)心的讀者可能會從上面的例子中發(fā)現(xiàn),同一個(gè)類被兩個(gè)不同的類加載器加載會出現(xiàn)問題,產(chǎn)生這種問題的原因是什么?在后面的類加載器的隔離性中會進(jìn)行解釋
類加載器的層級結(jié)構(gòu)是怎樣的?
如下是類加載器層級示意圖:
口說無憑,咱們來驗(yàn)證一下類加載器的層級結(jié)構(gòu),執(zhí)行如下代碼:
package com.hys;
public class Main {
public static void main(String[] args) {
ClassLoader c1 = User.class.getClassLoader();
System.out.println(c1);
ClassLoader c2 = c1.getParent();
System.out.println(c2);
ClassLoader c3 = c2.getParent();
System.out.println(c3);
}
}
執(zhí)行結(jié)果:
sun.misc.Launcher$AppClassLoader@b4aac2
sun.misc.Launcher$ExtClassLoader@140e19d
null
Process finished with exit code 0
執(zhí)行結(jié)果符合咱們的預(yù)期。
有讀者老爺發(fā)問了,怎么就符合預(yù)期了?結(jié)果中有個(gè)值為null,是個(gè)啥?Bootstrap ClassLoader呢?
原因在于頂級類加載器是Bootstrap ClassLoader,此加載器是C++代碼編寫,所以,使用Java獲取的時(shí)候,返回了null
類加載器的隔離性
JVM如何判斷一個(gè)類的唯一性?
JVM通過加載該類的類加載器和類名稱來確定一個(gè)類的唯一性。
這意味著我們使用兩個(gè)不同的類加載器加載同一個(gè)類,而在JVM層面上卻認(rèn)為這是兩個(gè)完全不同的類,從另一個(gè)層面看,相當(dāng)于類被類加載器所包裹與另個(gè)類加載器中的類進(jìn)行了隔離。
如何證明這種隔離的存在?執(zhí)行如下代碼:
package com.hys;
public class Main {
public static void main(String[] args) {
try{
User u1 = new User();
User u2 = new User();
// u1和u2都是同一個(gè)類加載器加載的
System.out.println("The same classloader: u1.isInstance(u2)" + u1.getClass().isInstance(u2));
MyClassLoader c1 = new MyClassLoader();
Class clazz = c1.loadClass("com.hys.User");
// clazz是MyClassLoader類加載器加載的
System.out.println("The different classloader: clazz.isInstance(u1)" + clazz.isInstance(u1));
}catch (Exception ex){
ex.printStackTrace();
}
}
}
執(zhí)行結(jié)果:
The same classloader: u1.isInstance(u2)true
The different classloader: clazz.isInstance(u1)false
Process finished with exit code 0
自定義類加載器
自定義類加載器,一般是繼承ClassLoader類,覆蓋findClass(String name)方法,在此方法中做查詢與讀取.class文件的操作,如下代碼:
package com.hys;
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String mRootDir;
public MyClassLoader() {
}
public MyClassLoader(String rootDir) {
mRootDir = rootDir;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassData(name);
if (data == null) {
throw new ClassNotFoundException();
}else{
return defineClass(name, data, 0, data.length);
}
}
private byte[]loadClassData(String className) {
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 4096;
byte[] buffer = new byte[bufferSize];
int bytesNumRead = 0;
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private String classNameToPath(String className) {
String classPath;
if(mRootDir == null || mRootDir.isEmpty())
classPath = className.replace('.', File.separatorChar) + ".class";
else
classPath = mRootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
return classPath;
}
}
我是青嵐之峰,如果讀完后覺的有所收獲,歡迎點(diǎn)贊加關(guān)注