類加載器
雙親委派機制是類加載器的一種工作方式,用于控制類的加載過程。類加載器是實作雙親委派機制的具體實體。
類加載器的主要任務是将類的位元組碼加載到記憶體中,并建立對應的類對象。當Java程式需要使用某個類時,類加載器會在類路徑中搜尋并加載類的位元組碼檔案。
「類加載器分為:」
- 引導類加載器:是由c++建立,在jvm建立之後加載,它負責加載JRE下lib目錄的jar。
- 擴充類加載器:負責加載JER下lib的ext,ExtClassLoader,它的父加載器是引導類加載器。
- 應用程式加載器:負責加載應用程式,AppClassLoader,它的父加載器是擴充類加載器。
- 自定義加載器:自己定義的,它的父加載器是應用程式加載器。
System.out.println("String加載類:" + String.class.getClassLoader());
System.out.println("DESKeyFactory加載類:" + com.sun.crypto.provider.DESCipher.class.getClassLoader());
System.out.println("Demo加載類:" + Demo.class.getClassLoader());
輸出:
String加載類:null
DESKeyFactory加載類:sun.misc.Launcher$ExtClassLoader@372f7a8d
Demo加載類:sun.misc.Launcher$AppClassLoader@18b4aac2
解釋:
String的加載類是引導類加載器加載的,引導類是由c++生成的,是以看不到,是個null。
ExtClassLoader是擴充類加載器。
AppClassLoader是應用程式加載器,一般加載類預設用到AppClassLoader。
「Launcher類」
Launcher類是Java虛拟機(JVM)啟動的入口類之一。它是JVM的一部分,并負責啟動Java應用程式。C++引導類加載完之後會先執行個體化Launcher。在執行個體化Launcher類的時候會建立ExtClassLoader加載器和AppClassLoader加載器。
class Launcher{
private ClassLoader loader;
private static Launcher launcher = new Launcher();
public static Launcher getLauncher() {
//預設AppClassLoader
return launcher;
}
public Launcher() {
//執行個體化擴充類加載器,它的父類沒有設定值
Launcher.ExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader();
//初始化AppClassLoader,并且将ext傳入設定為父類加載器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
}
... ...
}
我們也可以手動去調用加載某個類
Launcher.getLauncher().getClassLoader().loadClass("Demo.class")
雙親委派機制
雙親委派的邏輯代碼實作在Launcher類中loadClass方法中。源碼如下
Class<?> loadClass(String name, boolean resolve){
//從自己加載的類裡邊找
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
//遞歸父加載器加載
c = parent.loadClass(name, false);
} else {
//ext加載器的父是null,因為它的父加載器是引導類加載器,他是c++實作的,在java中部顯示
//這是最後一層,引導類加載器加載
c = findBootstrapClassOrNull(name);
}
if (c == null) {
//嘗試加載,由URLClassLoader類實作
c = findClass(name);
}
}
return c;
}
嘗試加載,#URLClassLoader.findClass源碼
Class<?> findClass(final String name){
String path = name.replace('.', '/').concat(".class");
//判斷是不是自己要加載的。jrt/ ext/
Resource res = ucp.getResource(path, false);
if (res != null) {
//真正加載class的方法
return defineClass(name, res);
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
雙親委派機制
當我們加載某個類或者手動調用loadClass時候具體流程:
Launcher.getLauncher().getClassLoader().loadClass("Demo")
因為getClassLoader預設傳回AppClassLoader,是以首先用AppClassLoader去找。
- AppClassLoader去自己已經加載的類裡邊找,如果沒有向上委托
- ExtClassLoader去自己已經加載的類裡邊找,如果沒有向上委托。
- 引導類去自己加載的類裡邊找,如果沒有,
- 引導類嘗試加載,判斷這個類是不是應該由自己加載,判斷是不是JRE包路徑下的,如果是加載并且傳回,不是向下傳播。
- ExtClassLoader判斷這個類是不是ext包路徑下的,如果是加載并傳回,不是向下傳播。
- AppClassLoader判斷這個類是不是classPath路徑下的,如果是加載。不是的話報錯ClassNotFound。
「為什麼先由AppClassLoader,而不是引導類開始?」
因為大部分類都是我們自己寫的,百分之95都是存放到AppClassLoader,隻有第一次加載的時候會多走一步,之後大部分都直接從AppClassLoader裡邊擷取。
「我們新增一個String類是否可以?」
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println(111);
}
}
執行報錯資訊:
錯誤: 在類 java.lang.String 中找不到 main 方法, 請将 main 方法定義為:
public static void main(String[] args)
否則 JavaFX 應用程式類必須擴充javafx.application.Application
我們新增的 java.lang.String,是能夠在引導類加載器的JRE包下邊找到的,并且傳回String類,這個傳回的是jdk自帶的類,并不是我們自己寫的類,是以會報找不到main方法。
「jdk為什麼要用雙親委派機制」?
jdk不讓修改自己内部的類,沙箱安全機制,防止核心API被篡改。
避免類的重複加載,父加載器已經加載完了,自己就不用再加載了。
自定義加載器
我們要自定義加載器,隻需要實作ClassLoader,重寫findClass就可以。
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
//真正的加載步驟
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader =
new MyClassLoader("C:/myClassLoader");
Class<?> clazz = myClassLoader.loadClass("com.bbk.code.User");
Object o = clazz.newInstance();
Method method = clazz.getDeclaredMethod("eat");
method.invoke(o);
System.out.println("目前MyClassLoader的類加載器:"+MyClassLoader.class.getClassLoader());
}
}
User類:
我們需要将User類編譯好的class檔案放到指定的目錄(C:/myClassLoader),需要建立目錄為com/bbk/code,執行main方法時候先要target下邊的User.class删除。
public class User {
public void eat(){
System.out.println("我是User類eat方法,我的加載器是"+User.class.getClassLoader());
}
}
執行輸出
我是User類eat方法,我的加載器是com.bbk.code.MyClassLoader@5a07e868
image-20230602150910148
自定義MyClassLoader繼承ClassLoader類初始話時候會先初始化父類,在這時候會給自定義MyClassLoader指派parent為AppClassLoader。具體代碼展現在:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
//預設複制為AppClassLoader
this.parent = parent;
... ...
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
... ...
return scl;
}
private static synchronized void initSystemClassLoader() {
... ...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
//傳回this.loader,引導類在加載Launcher類的時候會指派為AppClassLoader
scl = l.getClassLoader();
... ...
}
如何打破雙親委派機制
意思就是不在委托父加載,直接自己加載類。雙親委派的邏輯代碼實作在loadClass方法中。需要繼承ClassLoader重寫loadClass方法即可:。
@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);
//把去父類查找邏輯删除
//c = parent.loadClass(name, false);
if (c == null) {
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
重新執行main方法,出現以下錯誤:
java.io.FileNotFoundException: C:\myClassLoader\java\lang\Object.class (系統找不到指定的路徑。)
在加載User的時候,會先加載父類Object,我們打破雙親委派之後,自定義的加載器不存在Object是以會報錯。
我們把Object.class放入到我們的本地檔案夾中。重新執行報錯:
//禁止加載java.lang 包
java.lang.SecurityException: Prohibited package name: java.lang
java.lang必須由我們引導類加載器加載,沙箱安全。我們隻能讓我們的引導類去加載java.lang。修改代碼
@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) {
//我們自己的邏輯類名
if(!name.startsWith("com.bbk")){
//如果不是我們自己寫的類,還是走原來雙親委派邏輯
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
隻有當name是由com.bbk開頭的由我們加載器加載。其他的都由父加載器加載。不會報錯。
tomcat如何打破雙親委派機制
問題:當有兩個war包,一個war包依賴spring4.0,另一個war包依賴spring5.0,假如spring4.0的jar包加載在tomcat下的AppClassLoader中,那麼spring5.0就加載不了。需要解決這兩個spring共存,這兩個war包互相隔離。
類加載器定義:
tomcat類加載器
引導類 -> ext類加載器 -> appext類加載器 -> 加載tomcat公共類庫的類加載器 -> 有多少war包就生成多個webAppClassLoader(它就打破了雙親委派機制,隻是自己加載,不會向上委托)。