類加載器(ClassLoader)是負(fù)責(zé)讀取 Java 字節(jié)碼,并轉(zhuǎn)換成 java.lang.Class 類的一個(gè)實(shí)例的代碼模塊。
類加載器除了用于加載類外,還可用于確定類在Java虛擬機(jī)中的唯一性。在整個(gè)JVM里蝇率,縱然全限定名相同,若類加載器不同刽沾,則仍然不算作是同一個(gè)類本慕,無法通過 instanceOf 、equals 等方式的校驗(yàn)侧漓。
1. 類加載器
類加載器層級(jí)關(guān)系如下圖所示锅尘, Bootstrap、Extension布蔗、Application 三者并非是繼承關(guān)系藤违,而是子類加載器指派父類加載器為自己的 parent屬性。由于啟動(dòng)類加載器是內(nèi)嵌于 JVM 且無法被引用纵揍,因此 Extension Classloader 設(shè)置null為parent顿乒,即等同于指派啟動(dòng)類加載器為自己的父加載器。
1.1 Bootstrap ClassLoader
由C++語言實(shí)現(xiàn)的泽谨,并不是繼承自java.lang.ClassLoader淆游,沒有父類加載器。
加載Java核心類庫(kù)隔盛,如:$JAVA_HOME/jre/lib/rt.jar、resources.jar拾稳、sun.boot.class.path路徑下的包吮炕,用于提供JVM運(yùn)行所需的包。
它加載擴(kuò)展類加載器和應(yīng)用程序類加載器访得,并成為他們的父類加載器龙亲。
1.2 Extension ClassLoader
Java語言編寫,繼承自java.lang.ClassLoader悍抑,父類加載器為啟動(dòng)類加載器鳄炉。
負(fù)責(zé)加載java平臺(tái)中擴(kuò)展功能的一些jar包。從系統(tǒng)屬性:java.ext.dirs目錄中加載類庫(kù)搜骡,或者從JDK安裝目拂盯。錄:jre/lib/ext目錄下加載類庫(kù)。我們可以將我們自己的包放在以上目錄下记靡,就會(huì)自動(dòng)加載進(jìn)來了谈竿。
1.3 Application ClassLoader
Java語言編寫团驱,繼承自java.lang.ClassLoader,父類加載器為啟動(dòng)類加載器
它負(fù)責(zé)加載環(huán)境變量classpath或者系統(tǒng)屬性java.class.path指定路徑下的類庫(kù)
是程序中默認(rèn)的類加載器空凸,我們Java程序中的類嚎花,都是由它加載完成的。
我們可以通過ClassLoader#getSystemClassLoader()獲取并操作這個(gè)加載器呀洲。
1.4 User ClassLoader
Java語言編寫紊选,根據(jù)自身需要實(shí)現(xiàn)ClassLoader自定義加載class,如tomcat道逗、jboss都會(huì)根據(jù)j2ee規(guī)范自行實(shí)現(xiàn)ClassLoader兵罢。通過靈活定義classloader的加載機(jī)制,我們可以完成很多事情憔辫,例如解決類沖突問題趣些,實(shí)現(xiàn)熱加載以及熱部署,甚至可以實(shí)現(xiàn)jar包的加密保護(hù)贰您。實(shí)現(xiàn)自定義ClassLoader的示例參考章節(jié)3自定義類加載器
// App ClassLoader
System.out.println(this.getClass().getClassLoader());
// Ext ClassLoader
System.out.println(this.getClass().getClassLoader().getParent());
// Bootstrap ClassLoader
System.out.println(this.getClass().getClassLoader().getParent().getParent());
// Bootstrap ClassLoader
System.out.println(new String().getClass().getClassLoader());
輸出結(jié)果如下坏平,Bootstrap ClassLoader屬于JVM的范疇,所以是null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@3ac42916
null
null
3. 加載類的方式
1. 隱式加載:
通過new
隱式加載锦亦,創(chuàng)建對(duì)象時(shí)舶替,如果類未加載也會(huì)嘗試加載,例如 Student s = new Student();
會(huì)嘗試加載Student
類
2. 顯示加載:
- 通過
Class.forName(類全限定名)
加載
public static Class<?> forName(String className) throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
其中forName0()
方法調(diào)用中的參數(shù) true 表示要初始化該類杠园。包括:
- 執(zhí)行靜態(tài)代碼塊
- 初始化靜態(tài)域
例如com.mysql.jdbc.Driver
就通過靜態(tài)代碼塊想DriverManager中注冊(cè)自己顾瞪。當(dāng)然forName(String name, boolean initialize,ClassLoader loader)
也支持指定初始化(initialize)和類加載器(loader)
- 通過
ClassLoader
的loadClass()
方法加載類
4. 類加載機(jī)制
4.1 全盤負(fù)責(zé)
當(dāng)一個(gè)類加載器負(fù)責(zé)加載某個(gè)類時(shí),該類所依賴的和引用的其他類也將由該類加載器負(fù)責(zé)加載抛蚁,除非顯示使用另外一個(gè)類加載器來加載陈醒。
4.2 雙親委派
雙親委派(Parent Delegation),是一個(gè)非常糟糕的翻譯瞧甩,但是因?yàn)槭褂幂^廣所以一直沿用至今钉跷, 雙親委派也叫作“父類委托”,是指子類加載器如果沒有加載過該目標(biāo)類肚逸,就先委托父類加載器加載該目標(biāo)類爷辙,只有在父類加載器找不到字節(jié)碼文件的情況下才從自己的類路徑中查找并加載目標(biāo)類。
雙親委派的機(jī)制如ClassLoader中l(wèi)oadClass方法所示:
- findLoadedClass朦促,內(nèi)部調(diào)用native方法膝晾,在虛擬機(jī)內(nèi)存中查找是否已經(jīng)加載過此類
- 如果沒有加載,則委派父類加載务冕,對(duì)于Extension ClassLoader血当,parent是null,所以通過findBootstrapClassOrNull委派Bootstrap ClassLoader加載。
- 如果父類沒有加載歹颓,則通過findClass(name)自己嘗試加載坯屿。
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;
}
5. 自定義類加載器
“雙親委派”機(jī)制只是Java推薦的,并不是強(qiáng)制的機(jī)制巍扛。我們可以繼承java.lang.ClassLoader類领跛,實(shí)現(xiàn)自己的類加載器。如果想保持雙親委派模型撤奸,只需要重寫findClass(name)方法吠昭;如果想破壞雙親委派模型,則重寫loadClass(name)方法胧瓜。
如下MyClassLoader矢棚,通過重寫findClass,從MyClassLoader.setRoot 指定的目錄加載編譯后的.class 文件府喳。注意文件路徑不能是classpath路徑蒲肋, 防止要加載的類被Application ClassLoader加載。
public class MyClassLoader extends ClassLoader {
private String root;
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String className) {
// fileName處理邏輯需要根據(jù)實(shí)際情況修改钝满,保證能夠找到文件
String[] name = className.split("\\.");
String fileName = root + File.separatorChar + name[name.length - 1] + ".class";
try {
return Files.readAllBytes(Paths.get(fileName));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public void setRoot(String root) {
this.root = root;
}
public static void main(String[] args) {
MyClassLoader classLoader = new MyClassLoader();
classLoader.setRoot("指定目錄");
Class<?> testClass = null;
try {
testClass = classLoader.loadClass("類的全限定名");
Object object = testClass.newInstance();
System.out.println(object.getClass().getClassLoader());
System.out.println(object.getClass().getClassLoader().getParent());
} catch (Exception e) {
e.printStackTrace();
}
}
}
6. SPI機(jī)制是否打破雙親委派
關(guān)于SPI(Service Provider Interface)是否打破雙親委派兜粘,眾說紛紜,這里先拋出結(jié)論:沒有打破雙親委派弯蚜。
雙親委派機(jī)制是指孔轴,子類加載器加載類之前,先去父類加載器中查找碎捺,一直查到最基礎(chǔ)的啟動(dòng)類加載器路鹰,如果都沒有加載,則嘗試自行加載收厨。根據(jù)雙親委派原理晋柱,父類加載器加載的類對(duì)子類可見,反之則不成立诵叁。
這里以JDBC(Java Database Connectivity趣斤,Java數(shù)據(jù)庫(kù)連接池)為例進(jìn)行說明。使用JDBC的示例代碼如下:
String url = "jdbc:mysql://localhost:3306/test?serverTimezone=UTC";
Connection conn = DriverManager.getConnection(url, "root", "1234");
1. DriverManager加載
根據(jù)包名可知java.sql.DriverManager
是由啟動(dòng)類加載器加載黎休,在加載時(shí),通過靜態(tài)代碼塊調(diào)用loadInitialDrivers()
方法玉凯, loadInitialDrivers()
通過SPI的方式加載java.sql.Driver
的實(shí)現(xiàn)势腮,這里是com.mysql.jdbc.Driver
和com.mysql.fabric.jdbc.FabricMySQLDriver
,加載過程如下漫仆,省略部分非核心代碼:
private static void loadInitialDrivers() {
String drivers;
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
}
2. java.mysql.jdbc.Driver 加載
SPI的機(jī)制這里不展開講捎拯,核心原理是通過mysql-connector-java.jar中的META-INF/services/java.sql.Driver文件,找到java.sql.Driver
的具體實(shí)現(xiàn)盲厌,然后加載實(shí)現(xiàn)署照。
loadInitialDrivers()
中的load()
實(shí)現(xiàn)如下所示祸泪,注意通過Thread.currentThread().getContextClassLoader()
獲得應(yīng)用類加載器,賦值給loader
成員變量建芙。
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
SPI中没隘,最終通過Thread.currentThread().getContextClassLoader()
獲得的應(yīng)用類加載器加載com.mysql.jdbc.Driver
,代碼如下:
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
cn這里就是com.mysql.jdbc.Driver
和com.mysql.fabric.jdbc.FabricMySQLDriver
禁荸。調(diào)用c.newInstances時(shí)右蒲, 會(huì)執(zhí)行com.mysql.jdbc.Driver中的靜態(tài)代碼塊,即向DriverManager注冊(cè)自己赶熟。
以上穿插的源碼比較多瑰妄,又夾雜著SPI的源碼,所以比較混亂映砖,這里做一下總結(jié):
-
java.sql.DriverManager
是由啟動(dòng)類加載器加載间坐,在加載時(shí),通過SPI加載java.sql.Driver
的實(shí)現(xiàn)邑退,即com.mysql.jdbc.Driver
竹宋。 -
com.mysql.jdbc.Driver
是由應(yīng)用類加載器負(fù)責(zé)加載的 -
父類加載器(Bootstrap ClassLoader)通過線程上下文類加載器(ContextClassLoader)去請(qǐng)求子類加載器(Application ClassLoader)完成類加載的行為,看似打破了雙親委派模型來逆向使用類加載器瓜饥,晚上所有的打破雙親委派也是指這一過程逝撬。但是子類加載器的加載也是走雙親委派流程,先委托給父類加載器乓土,加載不到再嘗試自行加載宪潮,因此完全沒有破壞雙親委派。
image.png