一:ClassLoader
從JVM結(jié)構(gòu)圖中可以看到豪筝,類加載器的作用是將Java類文件加載到Java虛擬機腋颠。
只有當(dāng)類被加載進虛擬機內(nèi)存硅则,才能使用對應(yīng)的類厚者。
在Java中,類加載過程大概分為以下幾步:
- 通過全限類名獲取類文件字節(jié)數(shù)組。可來自本地文件膀篮、jar包、網(wǎng)絡(luò)等岂膳。
- 在方法區(qū)/元空間保存類的描述信息誓竿、靜態(tài)屬性。
- 在JVM堆中生成一個對應(yīng)的java.lang.Class對象谈截。
理解Java的類加載機制筷屡,對理解JVM有很大幫助。
二:Java默認(rèn)的類加載器
Java默認(rèn)提供三個類加載器簸喂,分別為:
- Bootstrap ClassLoader
- Extension ClassLoader
- App ClassLoader
Bootstrap ClassLoader 負(fù)責(zé)加載Java基礎(chǔ)類速蕊,主要是 %JRE_HOME%/lib/ 目錄下的rt.jar、resources.jar娘赴、charsets.jar等。
Extension ClassLoader 負(fù)責(zé)加載Java擴展類跟啤,主要是 %JRE_HOME%/lib/ext 目錄下的jar诽表。
App ClassLoader 負(fù)責(zé)加載當(dāng)前應(yīng)用的ClassPath中的所有類。
三個ClassLoader所負(fù)責(zé)加載的類隅肥,可以通過以下方式進行查看竿奏。
public class ClassPath {
public static void main(String[] args) {
System.out.println("Bootstrap ClassLoader path: ");
System.out.println(System.getProperty("sun.boot.class.path"));
System.out.println("----------------------------");
System.out.println("Extension ClassLoader path: ");
System.out.println(System.getProperty("java.ext.dirs"));
System.out.println("----------------------------");
System.out.println("App ClassLoader path: ");
System.out.println(System.getProperty("java.class.path"));
System.out.println("----------------------------");
}
}
具體原因,在源碼分析章節(jié)說明腥放。
其中Bootstrap ClassLoader是JVM級別的泛啸,由C++撰寫。
Extension ClassLoader和App ClassLoader都是Java類秃症。
JVM啟動Bootstrap ClassLoader候址,然后初始化sun.misc.Launcher。
接著种柑,Launcher初始化Extension ClassLoader和App ClassLoader岗仑。
三:源碼分析
sun.misc.Launcher類是Java程序的入口。
其構(gòu)造器如下:
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);
……
}
其中有兩行比較重要的代碼:
Launcher.ExtClassLoader.getExtClassLoader();
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
第一行初始化了ExtClassLoader聚请,但沒有指定其parent荠雕。
一些文章表示ExtClassLoader的父加載器是Bootstrap ClassLoader,這個說法其實并不完全準(zhǔn)確。
第二行初始化了AppClassLoader炸卑,指定ExtClassLoader作為其父加載器既鞠。并將AppClassLoader作為系統(tǒng)類加載器。
AppClassLoader將會成為自定義ClassLoader的默認(rèn)父加載器盖文。
具體邏輯可按照以下順序查看源代碼:
- Launcher類的getClassLoader()方法嘱蛋。
- ClassLoader類的initSystemClassLoader()方法。
- ClassLoader類的getSystemClassLoader()方法椅寺。
- ClassLoader類的ClassLoader()方法浑槽。
其中g(shù)etSystemClassLoader()方法的注釋為:
/**
* Returns the system class loader for delegation. This is the default
* delegation parent for new <tt>ClassLoader</tt> instances, and is
* typically the class loader used to start the application.
**/
ExtClassLoader和AppClassLoader都繼承了URLClassLoader類。
URLClassLoader支持從文件目錄和jar包加載class返帕。
ExtClassLoader和AppClassLoader都調(diào)用了父類的構(gòu)造函數(shù)桐玻。
public URLClassLoader(URL[] urls, ClassLoader parent,
URLStreamHandlerFactory factory)
URLClassLoader類中有個屬性為ucp,表示該ClassLoader負(fù)責(zé)搜索的路徑荆萤。
ExtClassLoader和AppClassLoader最大的不同镊靴,即它們負(fù)責(zé)的路徑不同。
/* The search path for classes and resources */
private final URLClassPath ucp;
查看源碼可得:
ExtClassLoader負(fù)責(zé)搜索的路徑為:
String var0 = System.getProperty("java.ext.dirs");
AppClassLoader負(fù)責(zé)搜索的路徑為:
String var1 = System.getProperty("java.class.path");
所以链韭,上一節(jié)可以通過這兩個方法獲取不同ClassLoader所負(fù)責(zé)加載的目錄偏竟。
此外,Bootstrap ClassLoader負(fù)責(zé)搜索的路徑為:
String bootClassPath = System.getProperty("sun.boot.class.path");
ClassLoader源碼
ClassLoader是一個抽象類敞峭,幾個主要的方法如下:
defineClass(String name, byte[] b, int off, int len)把字節(jié)數(shù)組b中的內(nèi)容轉(zhuǎn)換成Java類踊谋,返回的結(jié)果是java.lang.Class類的實例。
findClass(String name)查找名稱為name的類旋讹,返回的結(jié)果是java.lang.Class類的實例殖蚕。
loadClass(String name)加載名稱為name的類,返回的結(jié)果是java.lang.Class類的實例沉迹。
resolveClass(Class<?> c)鏈接指定的Java 類睦疫。
其中,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;
}
}
該方法主要的步驟如下:
- 指定全限類名進行加載蛤育,首先調(diào)用findLoadedClass(name)判斷當(dāng)前類加載器是否已經(jīng)加載該類。
- 如果沒有被加載葫松。則判斷當(dāng)前ClassLoader的父加載器是否為null瓦糕。如果不為null,則委托其父加載器進行加載进宝。如果為null刻坊,則使用Bootstrap ClassLoader進行加載。
- 如果父加載器或Bootstrap ClassLoader都無法加載党晋,則調(diào)用findClass(name)方法尋找需要加載的類谭胚。
此外徐块,loadClass方法還涉及加鎖的過程,使用ConcurrentHashMap對不同的全限類名進行加鎖灾而。
具體可查看getClassLoadingLock方法胡控。
四:雙親委托模式
Java類加載機制使用雙親委托模式。
一個ClassLoader加載一個類時旁趟,首先需要將任務(wù)委托給其父加載器昼激,直到Bootstrap ClassLoader。
如果父加載器未加載該類锡搜,則逐層返回給委托發(fā)起者即當(dāng)前ClassLoader進行加載橙困。
在正常應(yīng)用中,用戶不自定義類加載器耕餐。
類加載工作首先由App ClassLoader發(fā)起凡傅,然后委托給Extension ClassLoader,最后委托給Bootstrap ClassLoader肠缔。
首先夏跷,通過一個例子了解三個ClassLoader所負(fù)責(zé)加載的類和雙親委托模式。
新建一個jar包明未,名為acai-cl.jar槽华,包中有個簡單的Person類。
寫一個簡單的程序輸出person對象所對應(yīng)的ClassLoader趟妥。
import com.acai.Person;
public class TestClassLoader {
public static void main(String[] args) {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
}
}
測試一:將jar包引入項目
對應(yīng)輸出:
sun.misc.Launcher$AppClassLoader@18b4aac2
可以看到猫态,位于ClassPath的類,是由App ClassLoader負(fù)責(zé)加載披摄。
測試二:將jar包復(fù)制到%JRE_HOME%/lib/ext目錄
對應(yīng)輸出:
sun.misc.Launcher$ExtClassLoader@4cc77c2e
可以得出懂鸵,Extension ClassLoader負(fù)責(zé)加載%JRE_HOME%/lib/ext目錄下的類。
加載Person類時行疏,會首先嘗試使用App ClassLoader進行加載。
由于雙親委托模式套像,最終委托到Extension ClassLoader贞让,而其負(fù)責(zé)的目錄%JRE_HOME%/lib/ext下存在Person類销部,則進行了類加載操作。
測試三:將jar包追加到Bootstrap ClassLoader加載路徑上
使用參數(shù):-Xbootclasspath/a:d:\acai-cl.jar撒妈,將jar包追加到Bootstrap ClassLoader加載路徑挟阻。
對應(yīng)輸出:
null
可以看出坷备,Person類的加載工作竟秫,最終被委托到了Bootstrap ClassLoader。
注:Bootstrap ClassLoader由C++撰寫纽谒。由Bootstrap ClassLoader負(fù)責(zé)加載的類,其getClassLoader()方法輸出為null。
可以嘗試輸出String類的類加載器眼虱。
System.out.println(String.class.getClassLoader());
接下來甥厦,再通過debug來驗證雙親委托模式。
還是原來那個簡單的demo疚鲤。
import com.acai.Person;
public class Test {
public static void main(String[] args) {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
}
}
在ClassLoader類的loadClass方法上打斷點。
可以看出缘挑,類的加載過程符合從下到上委托集歇,最終會被委托到Bootstrap ClassLoader。
同時符合從上到下加載语淘,每一層ClassLoader都會嘗試進行加載鬼悠。最終由App ClassLoader加載了Person類。
接著亏娜,嘗試加載一個特殊的類:Splash.class。
Splash類位于jfxrt.jar蹬挺,這個jar包在%JRE_HOME%/lib/ext目錄下维贺。
import com.sun.javafx.applet.Splash;
public class ExtTest {
public static void main(String[] args) {
Splash splash = new Splash(null);
System.out.println(splash.getClass().getClassLoader());
}
}
對應(yīng)輸出:
sun.misc.Launcher$ExtClassLoader@330bedb4
毫無疑問,Splash類應(yīng)該由Extension ClassLoader進行加載巴帮。
但其加載過程溯泣,仍然會從默認(rèn)的系統(tǒng)類加載器App ClassLoader開始虐秋。
可以通過debug進行查看。
Splash類加載的過程會被委托到Bootstrap ClassLoader垃沦,但Bootstrap ClassLoader并不負(fù)責(zé)加載%JRE_HOME%/lib/ext目錄下的類客给。最終由Extension ClassLoader進行加載。
很多文章在闡述三個ClassLoader之間的關(guān)系時候肢簿,會給出一個getParent操作的demo靶剑。
并且認(rèn)為Bootstrap ClassLoader是Extension ClassLoader的父加載器。
Extension ClassLoader是App ClassLoader的父加載器池充。
App ClassLoader是自定義類加載器的父加載器桩引。
這樣的解釋基本正確,但Bootstrap ClassLoader和Extension ClassLoader之間的關(guān)系需要額外解釋收夸。
由于Bootstrap ClassLoader并不是使用Java編寫,故無法指定Extension ClassLoader的parent為Bootstrap ClassLoader卧惜。
這一層關(guān)系在ClassLoader的loadClass方法中做了彌補厘灼。
在加載類時,會判斷當(dāng)前ClassLoader的父加載器是否為null咽瓷,為null則使用Bootstrap ClassLoader進行加載设凹。
在Java提供的三個默認(rèn)類加載器中,父加載器為null的只有Extension ClassLoader忱详。
該過程可參考ClassLoader的loadClass方法围来。
為什么使用雙親委托模式?
網(wǎng)上很多例子是關(guān)于String類匈睁。假設(shè)自己寫一個java.lang.String類监透,使用雙親委托模式可以防止這個問題。
但其實雙親委托模式可以被打破航唆,而真正阻止自定義java.lang.String的是“安全機制”胀蛮。
這里嘗試自定義java.lang.String類,并使用自定義ClassLoader進行加載糯钙。
package java.lang;
public class String {
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
public class StringClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if ("java.lang.String".equals(name)) {
return findClass(name);
} else {
return super.loadClass(name);
}
}
@Override
public Class<?> findClass(String s) throws ClassNotFoundException {
try {
byte[] classBytes = Files.readAllBytes(Paths.get("d:/String.class"));
return defineClass(s, classBytes, 0, classBytes.length);
} catch (IOException e) {
throw new ClassNotFoundException(s);
}
}
public static void main(String[] args) throws ClassNotFoundException {
StringClassLoader stringClassLoader = new StringClassLoader();
Class clazz = stringClassLoader.loadClass("java.lang.String", false);
System.out.println(clazz.getClassLoader());
}
}
該自定義類加載器破壞了雙親委托機制粪狼,具體方式將在下個章節(jié)說明。
輸出結(jié)果為:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
可以看到任岸,在被findClass方法調(diào)用的defineClass中有這么一段:
if ((name != null) && name.startsWith("java.")) {
throw new SecurityException
("Prohibited package name: " +
name.substring(0, name.lastIndexOf('.')));
}
它會檢查當(dāng)前加載類的全限類名是否以java.開頭再榄,這也是一種安全機制。
如果按照網(wǎng)上的說法享潜,java.lang.String被Bootstrap ClassLoader加載困鸥,demo中自定義的類加載器會被略過,不會輸出異常剑按。
所以說疾就,雙親委托模式的作用只是防止類重復(fù)加載澜术。
五:自定義ClassLoader
多數(shù)情況下,Java默認(rèn)的三個類加載器已經(jīng)可以滿足需求猬腰。
自定義類加載器則可以實現(xiàn)額外的需求鸟废,例如:
- 從網(wǎng)絡(luò)文件加載類。
- 從任意目錄加載類姑荷。
- 對字節(jié)碼文件做加密處理盒延,由自定義類加載器做解密。
實現(xiàn)自定義類加載器的主要步驟為:
- 繼承ClassLoader類厢拭。如果只是從目錄或者jar包加載類兰英,也可以選擇繼承URLClassLoader類。
- 重寫findClass方法供鸠。
- 在重寫的findClass方法中畦贸,無論用何種方法,獲取類文件對應(yīng)的字節(jié)數(shù)組楞捂,然后調(diào)用defineClass方法轉(zhuǎn)換成類實例薄坏。
自定義類加載器真正好玩的是打破雙親委托機制,也是很多面試官會問到的問題寨闹。
上文提到類加載雙親委托模式實現(xiàn)位于ClassLoader的loadClass方法胶坠,想要破壞這個機制,則需要重寫該方法繁堡。
打破雙親委托模式的確有一定的實用價值沈善。
比如有兩個class文件,或者兩個jar包椭蹄。
其中兩個類的全限類名都一樣闻牡,如果需要同時使用這兩個類,則需要打破雙親委托模式绳矩。
有兩個Person類罩润,它們的全限類名均為com.acai.Person,唯一的區(qū)別是sayHello()方法輸出的內(nèi)容略有不同翼馆。
package com.acai;
import lombok.Data;
@Data
public class Person {
private String name;
private Integer age;
public void sayHello() {
System.out.println("Hello, this is Person in acai-cl");
}
}
package com.acai;
import lombok.Data;
@Data
public class Person {
private String name;
private Integer age;
public void sayHello() {
System.out.println("Hello, this is Person in acai-cl2");
}
}
將兩個Person所在的項目打成jar包割以。
常規(guī)操作是,把兩個jar包都引進項目应媚。
寫一個小小的demo严沥。
import com.acai.Person;
public class Main {
public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
person.sayHello();
}
}
對應(yīng)輸出為:
sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl
可以看到,demo中默認(rèn)使用了acai-cl.jar中的Person類中姜。
如果想要使用acai-cl2.jar中的Person類消玄,則想到新建一個ClassLoader。
需要從jar包加載類,則優(yōu)先想到URLClassLoader莱找。
import com.acai.Person;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
person.sayHello();
URL url = new File("d:/acai-cl2.jar").toURI().toURL();
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Thread.currentThread().setContextClassLoader(loader);
Class<?> clazz = loader.loadClass("com.acai.Person");
System.out.println(clazz.getClassLoader());
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(clazz.newInstance());
}
}
對應(yīng)輸出:
sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-clsun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl
可以看出,即使是指定了使用acai-cl2.jar嗜桌,輸出的仍然是acai-cl.jar中Person的sayHello奥溺。
原因是由于兩個Person類擁有一樣的全限類名。
加載第二個Person的時候骨宠,發(fā)現(xiàn)自定義類加載器的父類加載器App ClassLoader已經(jīng)加載了com.acai.Person浮定。
所以直接返回該類,即為acai-cl.jar中的Person類层亿。
于是想到桦卒,新建ClassLoader,并且破壞雙親委托機制匿又,重新loadClass方法方灾。
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;
public class MyClassLoader extends URLClassLoader {
public MyClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public MyClassLoader(URL[] urls) {
super(urls);
}
public MyClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
if (name.equals("com.acai.Person")) {
return super.findClass(name);
} else {
return super.loadClass(name);
}
}
}
在MyClassLoader中重寫了loadClass方法,當(dāng)加載的類名等于com.acai.Person時碌更,直接調(diào)用findClass方法裕偿,繞過雙親委托機制。
這里需要一個if判斷痛单,表示只有在加載com.acai.Person時才破壞雙親委托嘿棘。
因為在加載一個類時,會同時加載它的父類旭绒。
Person的父類為java.lang.Object鸟妙。
直接用自定義類加載器加載Object類,會拋出SecurityException異常挥吵。
于是重父,寫一個demo。
import com.acai.Person;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
public class Main {
public static void main(String[] args) throws Exception {
Person person = new Person();
System.out.println(person.getClass().getClassLoader());
person.sayHello();
URL url = new File("d:/acai-cl2.jar").toURI().toURL();
URLClassLoader loader = new URLClassLoader(new URL[]{url});
Thread.currentThread().setContextClassLoader(loader);
Class<?> clazz = loader.loadClass("com.acai.Person");
System.out.println(clazz.getClassLoader());
Method method = clazz.getDeclaredMethod("sayHello");
method.invoke(clazz.newInstance());
URL url2 = new File("d:/acai-cl2.jar").toURI().toURL();
MyClassLoader myClassLoader = new MyClassLoader(new URL[]{url2});
Class<?> clazz2 = myClassLoader.loadClass("com.acai.Person");
System.out.println(clazz2.getClassLoader());
Method method2 = clazz2.getDeclaredMethod("sayHello");
method2.invoke(clazz2.newInstance());
}
}
sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-clsun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-clMyClassLoader@5e2de80c
Hello, this is Person in acai-cl2
可以看到蔫劣,acai-cl2.jar中的Person類被正確加載坪郭。
得出,可以通過自定義ClassLoader脉幢,重寫loadClass歪沃,破壞雙親委托機制。
六:參考資料
[1] Java Garbage Collection Basics
[2] java classloader是怎么加載自身到內(nèi)存里面執(zhí)行的嫌松?
[3] 詳細(xì)深入分析 Java ClassLoader 工作機制
[4] 深入分析Java ClassLoader原理
[5] 深入探討 Java 類加載器
[6] 深度分析Java的ClassLoader機制(源碼級別)
[7] Java類加載原理與ClassLoader使用總結(jié)
[8] 實現(xiàn)java classloader 動態(tài)加載jar包
[9] ClassLoader的基礎(chǔ)詳解