Java類加載器與雙親委派模型
1、類加載器
類加載階段中通過類的全限定名獲取定義此類的二進制字節流的動作代碼。然後在堆上創建一個java.lang.Class
對象。
對於任何一個類,都需要由加載它的類加載器和這個類來確立其在JVM中的唯一性。也就是說,兩個類來源於同一個Class文件,並且被同一個類加載器加載,這兩個類才相等。
分類,常用三種:
- 啓動類加載器(Bootstrap ClassLoader): 加載目錄 <JAVA_HOME>\lib
- 擴展類加載器(Extension CLassLoader): <JAVA_HOME>\lib\ext
- 應用程序類加載器(Application ClassLoader): 用戶目錄classpath,用戶可以直接使用的類加載器,程序中若沒有自定義的加載器則默認使用這個。
2、雙親委派模型
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的加載器都是如此,因此所有的類加載請求都會傳給頂層的啓動類加載器,只有當父加載器反饋自己無法完成該加載請求(該加載器的搜索範圍中沒有找到對應的類)時,子加載器纔會嘗試自己去加載。
優點:
解決了各個類加載器的基礎類統一問題,越基礎的類由越上層的類加載器進行加載。
例如java.lang.Object類,無論哪個類加載器去加載該類,最終都是由啓動類加載器進行加載,因此Object類在程序的各種類加載器環境中都是同一個類。否則的話,如果不使用該模型的話,如果用戶自定義一個java.lang.Object類且存放在classpath中,那麼系統中將會出現多個Object類,應用程序也會變得很混亂。
缺點:
當基礎類想要調用回下層的用戶代碼時無法委派子類加載器進行類加載。解決方式:線程上下文類加載器,這裏不作介紹。
如果我們自定義一個rt.jar中已有類的同名Java類,會發現JVM可以正常編譯,但該類永遠無法被加載運行。 在rt.jar包中的java.lang.ClassLoader類中,我們可以查看類加載實現過程的代碼,具體源碼如下:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先檢查該name指定的class是否有被加載
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
// 如果parent不爲null,則調用parent的loadClass進行加載
c = parent.loadClass(name, false);
} else {
// parent爲null,則調用BootstrapClassLoader進行加載
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果仍然無法加載成功,則調用自身的findClass進行加載
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;
}
}
可見,雙親之一爲父類加載器,之二是啓動類加載器
注意:
因爲啓動類加載器無法被java程序直接引用(c++實現),所以代碼中用null代表啓動類加載器。
注意到調用自身的findClass()方法,該方法是一個空方法:
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
說明該方法需要開發者自己去實現,否則拋出異常,詳情見下節“自定義類的加載器”
3、自定義類加載器
方式:
- 繼承
java.lang.ClassLoader
類- 重寫
findClass()
方法,返回值即爲java.lang.Class
類的實例,不建議重寫loadClass()
方法(破壞了雙親委派機制)。
java.lang.ClassLoader 類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的字節碼,然後從這些字節碼中定義出一個 Java 類,即 java.lang.Class 類的一個實例。
示例代碼:
自定義的類加載器MyClassLoader
import java.io.*;
/**
* @description:
* @Author: JachinDo
* @Date: 2019/12/03 20:08
*/
public class MyClassLoader extends ClassLoader {
// 加載器的名稱
private String name;
// 類存放的路徑,因爲idea的緣故,所以路徑後面爲/out/production/jvm/
private String path = "/Users/jc/IdeaProjects/jvm/out/production/jvm/";
public MyClassLoader(ClassLoader parent, String name) {
super(parent);
this.name = name;
}
public MyClassLoader(String name) {
this.name = name;
}
// 重寫findClass方法
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] data = loadClassData(name);
// 調用ClassLoader中的方法
return this.defineClass(name, data, 0, data.length);
}
public byte[] loadClassData(String name) {
// 將全限定類名轉換爲文件的路徑
name = name.replace(".", "/");
try {
FileInputStream fis = new FileInputStream(new File(path + name + ".class"));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
int flag = 0;
// 將類的.class文件讀如到一個byte數組中
while ((flag = fis.read()) != -1) {
bos.write(flag);
}
return bos.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
需要加載的類:
import java.io.Serializable;
/**
* @description:
* @Author: JachinDo
* @Date: 2019/11/28 13:47
*/
public class Student{
private int id;
private String name;
public Student(int id, String name) {
this.id = id;
this.name = name;
}
public Student() {
}
public void shuchu() {
System.out.println("kkkkkkkkk " + id);
}
}
使用自定義類加載器加載Student
類,(前提Student類已經編譯爲.class文件)
/**
* @description:
* @Author: JachinDo
* @Date: 2019/12/01 20:48
*/
public class Main {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader mcl = new MyClassLoader("myClassLoader1");
// 根據全限定名加載,注意,這裏調用loadClass方法
Class<?> clazz = mcl.loadClass("Student");
Student student = (Student)clazz.newInstance();
student.shuchu();
System.out.println(student.getClass().getClassLoader());
}
}
注意:第14行爲查看加載該實例對象的類的類加載器,結果爲:
sun.misc.Launcher$AppClassLoader@18b4aac2
4、ClassLoader隔離
舉例:
如果兩個”類”的全限定名相同,但不是由一個 ClassLoader 加載,是無法將一個類的實例強轉爲另外一個類的。這就是ClassLoader隔離。
5、如何避免雙親委派
由於系統自帶的三個類加載器都加載特定目錄下的類,如果我們自己的類加載器放在一個特殊的目錄,那麼系統的加載器就無法加載,也就是最終還是由我們自己的加載器加載。
參考:https://juejin.im/post/5a810b0e5188257a5c606a85