JVM類加載器之ClassLoader的使用詳解
類加載器
概述
類加載器負(fù)責(zé)讀取Java字節(jié)代碼,并轉(zhuǎn)換成java.lang.Class類的一個(gè)實(shí)例的代碼模塊。
類加載器除了用于加載類外,還可用于確定類在Java虛擬機(jī)中的唯一性。
任意一個(gè)類,都由加載它的類加載器和這個(gè)類本身一同確定其在 Java 虛擬機(jī)中的唯一性,每一個(gè)類加載器,都有一個(gè)獨(dú)立的類名稱空間,而不同類加載器中是允許同名(指全限定名相同)類存在的。
比較兩個(gè)類是否“相等”,前提是這兩個(gè)類由同一個(gè)類加載器加載,否則,即使這兩個(gè)類來源于同一個(gè)Class 文件,被同一個(gè)虛擬機(jī)加載,只要加載它們的類加載器不同,那么這兩個(gè)類就必定不相等。
這里“相等”是指:類的Class對(duì)象的equals()方法、isInstance()方法的返回結(jié)果,使用instanceof關(guān)鍵字做對(duì)象所屬關(guān)系判定等情況。
加載器的種類
1.啟動(dòng)類加載器:Bootstrap ClassLoader
最頂層的加載類,由 C++實(shí)現(xiàn),負(fù)責(zé)加載%JAVA_HOME%/lib目錄下的jar包和類或者被 -Xbootclasspath參數(shù)指定的路徑中的所有類。
2.拓展類加載器:Extension ClassLoader
負(fù)責(zé)加載java平臺(tái)中擴(kuò)展功能的一些jar包,如加載%JRE_HOME%/lib/ext目錄下的jar包和類,或-Djava.ext.dirs所指定的路徑下的jar包。
3.系統(tǒng)類加載器/應(yīng)用程序加載器:App ClassLoader
負(fù)責(zé)加載當(dāng)前應(yīng)用classpath中指定的jar包及-Djava.class.path所指定目錄下的類和jar包。開發(fā)者可以直接使用這個(gè)類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個(gè)就是程序中默認(rèn)的類加載器。
4.自定義類加載器:Custom ClassLoader
通過java.lang.ClassLoader的子類自定義加載class,屬于應(yīng)用程序根據(jù)自身需要自定義的ClassLoader,如tomcat、jboss都會(huì)根據(jù)j2ee規(guī)范自行實(shí)現(xiàn)ClassLoader
驗(yàn)證不同加載器
每個(gè)類加載都有一個(gè)父類加載器,可以通過程序來驗(yàn)證
public static void main(String[] args) {
// App ClassLoader
System.out.println(new User().getClass().getClassLoader());
// Ext ClassLoader
System.out.println(new User().getClass().getClassLoader().getParent());
// Bootstrap ClassLoader
System.out.println(new User().getClass().getClassLoader().getParent().getParent());
// Bootstrap ClassLoader
System.out.println(new String().getClass().getClassLoader());
}
AppClassLoader的父類加載器為ExtClassLoader, ExtClassLoader的父類加載器為 null,null 并不代表ExtClassLoader沒有父類加載器,而是 BootstrapClassLoader 。
sun.misc.Launcher$AppClassLoader@18b4aac2 sun.misc.Launcher$ExtClassLoader@5fdef03a null null
核心方法
查看類ClassLoader的loadClass方法
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 檢查類是否已經(jīng)加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 父加載器不為空,調(diào)用父加載器loadClass()方法處理
if (parent != null) {
// 讓上一層加載器進(jìn)行加載
c = parent.loadClass(name, false);
} else {
// 父加載器為空,使用啟動(dòng)類加載器 BootstrapClassLoader 加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 拋出異常說明父類加載器無法完成加載請(qǐng)求
// 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();
// 調(diào)用此類加載器所實(shí)現(xiàn)的findClass方法進(jìn)行加載
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方法是當(dāng)字節(jié)碼加載到內(nèi)存后進(jìn)行鏈接操作,對(duì)文件格式和字節(jié)碼驗(yàn)證,并為 static 字段分配空間并初始化,符號(hào)引用轉(zhuǎn)為直接引用,訪問控制,方法覆蓋等
resolveClass(c);
}
return c;
}
}
JVM類加載機(jī)制的三種方式
全盤負(fù)責(zé)
當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè)Class時(shí),該Class所依賴的和引用的其他Class也將由該類加載器負(fù)責(zé)載入,除非顯示使用另外一個(gè)類加載器來載入
注意:
系統(tǒng)類加載器AppClassLoader加載入口類(含有main方法的類)時(shí),會(huì)把main方法所依賴的類及引用的類也載入。只是調(diào)用了ClassLoader.loadClass(name)方法,并沒有真正定義類。真正加載class字節(jié)碼文件生成Class對(duì)象由雙親委派機(jī)制完成。
父類委托、雙親委派
父類委托即雙親委派,雙親委派模型是描述類加載器之間的層次關(guān)系。它要求除了頂層的啟動(dòng)類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。父子關(guān)系一般不會(huì)以繼承的關(guān)系實(shí)現(xiàn),而是以組合關(guān)系來復(fù)用父加載器的代碼。
雙親委派模型是指:子類加載器如果沒有加載過該目標(biāo)類,就先委托父類加載器加載該目標(biāo)類,只有在父類加載器找不到字節(jié)碼文件的情況下才從自己的類路徑中查找并裝載目標(biāo)類。
雙親委派模型的好處
保證Java程序的穩(wěn)定運(yùn)行,避免類的重復(fù)加載:JVM區(qū)分不同類的方式不僅僅根據(jù)類名,相同的類文件被不同的類加載器加載產(chǎn)生的是兩個(gè)不同的類
保證Java核心API不被篡改:如果沒有使用雙親委派模型,而是每個(gè)類加載器加載自己的話就會(huì)出現(xiàn)一些問題,如編寫一個(gè)稱為java.lang.Object 類,程序運(yùn)行時(shí),系統(tǒng)就會(huì)出現(xiàn)多個(gè)不同的Object類。反之使用雙親委派模型:無論使用哪個(gè)類加載器加載,最終都會(huì)委派給最頂端的啟動(dòng)類加載器加載,從而使得不同加載器加載的Object類都是同一個(gè)。
雙親委派機(jī)制加載Class的具體過程:
1. ClassLoader先判斷該Class是否已加載,如果已加載,則返回Class對(duì)象,如果沒有則委托給父類加載器
2. 父類加載器判斷是否加載過該Class,如果已加載,則返回Class對(duì)象,如果沒有則委托給祖父類加載器
3. 依此類推,直到始祖類加載器(引用類加載器)
4. 始祖類加載器判斷是否加載過該Class,如果已加載,則返回Class對(duì)象
如果沒有則嘗試從其對(duì)應(yīng)的類路徑下尋找class字節(jié)碼文件并載入
如果載入成功,則返回Class對(duì)象;如果載入失敗,則委托給始祖類加載器的子類加載器
5. 始祖類加載器的子類加載器嘗試從其對(duì)應(yīng)的類路徑下尋找class字節(jié)碼文件并載入
如果載入成功,則返回Class對(duì)象;如果載入失敗,則委托給始祖類加載器的孫類加載器
6. 依此類推,直到源ClassLoader
7. 源ClassLoader嘗試從其對(duì)應(yīng)的類路徑下尋找class字節(jié)碼文件并載入
如果載入成功,則返回Class對(duì)象;如果載入失敗,源ClassLoader不會(huì)再委托其子類加載器,而是拋出異常
注意:
雙親委派機(jī)制是Java推薦的機(jī)制,并不是強(qiáng)制的機(jī)制??梢岳^承java.lang.ClassLoader類,實(shí)現(xiàn)自己的類加載器。如果想保持雙親委派模型,應(yīng)該重寫findClass(name)方法;如果想破壞雙親委派模型,可以重寫loadClass(name)方法。
緩存機(jī)制
緩存機(jī)制將會(huì)保證所有加載過的Class都將在內(nèi)存中緩存,當(dāng)程序中需要使用某個(gè)Class時(shí),類加載器先從內(nèi)存的緩存區(qū)尋找該Class,只有緩存區(qū)不存在,系統(tǒng)才會(huì)讀取該類對(duì)應(yīng)的二進(jìn)制數(shù)據(jù),并將其轉(zhuǎn)換成Class對(duì)象,存入緩存區(qū)。
對(duì)于一個(gè)類加載器實(shí)例來說,相同全名的類只加載一次,即loadClass方法不會(huì)被重復(fù)調(diào)用。因此,這就是為什么修改Class后,必須重啟JVM,程序的修改才會(huì)生效的原因。
JDK8使用的是直接內(nèi)存,所以會(huì)用到直接內(nèi)存進(jìn)行緩存。因此,類變量為什么只會(huì)被初始化一次的原因。
打破雙親委派
在加載類的時(shí)候,會(huì)一級(jí)一級(jí)向上委托,判斷是否已經(jīng)加載,從自定義類加載器 --> 應(yīng)用類加載器 --> 擴(kuò)展類加載器 --> 啟動(dòng)類加載器,如果到最后都沒有加載這個(gè)類,則回去加載自己的類。

雙親委派模型并不是強(qiáng)制模型,而且會(huì)帶來一些些的問題。例如:java.sql.Driver類,JDK只能提供一個(gè)規(guī)范接口,而不能提供實(shí)現(xiàn)。提供實(shí)現(xiàn)的是實(shí)際的數(shù)據(jù)庫提供商,提供商的庫不可能放JDK目錄里。
重寫loadclass方法
自定義類加載,重寫loadclass方法,即可破壞雙親委派機(jī)制
因?yàn)殡p親委派的機(jī)制都是通過這個(gè)方法實(shí)現(xiàn)的,這個(gè)方法可以指定類通過什么類加載器來進(jìn)行加載,所有如果改寫加載規(guī)則,相當(dāng)于打破雙親委派機(jī)制
import cn.ybzy.demo.Test;
import java.io.*;
public class MyClassLoader extends ClassLoader {
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData;
try {
classData = loadClassData(name);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) throws IOException {
String replace = className.replace('.', File.separatorChar);
String path = ClassLoader.getSystemResource("").getPath() + replace + ".class";
InputStream inputStream = null;
ByteArrayOutputStream byteArrayOutputStream = null;
try {
inputStream = new FileInputStream(path);
byteArrayOutputStream = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (byteArrayOutputStream != null) {
byteArrayOutputStream.close();
}
if (inputStream != null) {
inputStream.close();
}
}
return null;
}
@Override
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 {
// 修改classloader的原雙親委派邏輯,從而打破雙親委派
if (name.startsWith("cn.ybzy.demo")) {
c = findClass(name);
} else {
c = this.getParent().loadClass(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;
}
}
}
public static void main(String[] args) throws ClassNotFoundException {
MyClassLoader classLoader = new MyClassLoader();
Class<?> aClass = classLoader.loadClass(Test.class.getName());
System.out.println(aClass.getClassLoader());
}
cn.ybzy.demo.MyClassLoader@2f410acf
自定義類加載器
自定義類加載器的核心在于對(duì)字節(jié)碼文件的獲取,如果是加密的字節(jié)碼則需要在類中對(duì)文件進(jìn)行解密。
準(zhǔn)備字節(jié)碼文件
創(chuàng)建Test類,同時(shí)進(jìn)行javac Test.class編譯成字節(jié)碼文件,放到目錄下:D:\Temp\cn\ybzy\demo
package cn.ybzy.demo;
public class Test {
public static void main(String[] args) {
System.out.println("Test...");
}
}
創(chuàng)建自定義類加載器
import java.io.*;
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData;
try {
classData = loadClassData(name);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) throws IOException {
String fileName = root + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
InputStream inputStream = null;
ByteArrayOutputStream byteArrayOutputStream = null;
try {
inputStream = new FileInputStream(fileName);
byteArrayOutputStream = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length = 0;
while ((length = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, length);
}
return byteArrayOutputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (byteArrayOutputStream != null) {
byteArrayOutputStream.close();
}
if (inputStream != null) {
inputStream.close();
}
}
return null;
}
public String getRoot() {
return root;
}
public void setRoot(String root) {
this.root = root;
}
}
執(zhí)行測(cè)試
啟動(dòng)main方法,執(zhí)行測(cè)試
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("D:\\Temp");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("cn.ybzy.demo.Test");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
cn.ybzy.demo.MyClassLoader@5679c6c6
將Test類放到項(xiàng)目類路徑下,由于雙親委托機(jī)制的存在,會(huì)直接導(dǎo)致該類由 AppClassLoader 加載,而不會(huì)通過自定義類加載器來加載

sun.misc.Launcher$AppClassLoader@18b4aac2
注意事項(xiàng)
1、這里傳遞文件名需要是類的全限定性名稱,因?yàn)閐efineClass方法是按這種方式/格式進(jìn)行處理
因此,若沒有全限定名,需要將類的全路徑加載進(jìn)去
2、不要重寫loadClass方法,因?yàn)檫@樣容易破壞雙親委托模式
3、Test類本身可以被AppClassLoader類加載,因此不能把Test.class放在類路徑下
否則,由于雙親委托機(jī)制的存在,會(huì)直接導(dǎo)致該類由AppClassLoader加載,而不會(huì)通過自定義類加載器來加載
以上就是JVM類加載器之ClassLoader的使用詳解的詳細(xì)內(nèi)容,更多關(guān)于JVM類加載器ClassLoader的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springboot植入pagerHelper的超詳細(xì)教程
這篇文章主要介紹了springboot植入pagerHelper的超詳細(xì)教程,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-01-01
Spring?Boot?ORM?框架JPA使用與連接池?Hikari詳解
這篇文章主要介紹了SpringBoot?ORM框架JPA與連接池Hikari,主要就是介紹JPA?的使用姿勢(shì),本文結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-08-08
基于JSON和java對(duì)象的互轉(zhuǎn)方法
下面小編就為大家?guī)硪黄贘SON和java對(duì)象的互轉(zhuǎn)方法。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
基于Spring Mvc實(shí)現(xiàn)的Excel文件上傳下載示例
本篇文章主要介紹了基于Spring Mvc實(shí)現(xiàn)的Excel文件上傳下載示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02
Java Web項(xiàng)目中驗(yàn)證碼功能的制作攻略
使用servlet制作驗(yàn)證碼中最關(guān)鍵的部分是緩存的使用,驗(yàn)證session中的字符串,接下來我們就來看一下Java Web項(xiàng)目中驗(yàn)證碼功能的制作攻略2016-05-05
Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(46)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧,希望可以幫到你2021-08-08

