天天看點

面試官:說一下雙親委派機制和如何打破雙親委派機制?

作者:Java面試技術棧

類加載器

雙親委派機制是類加載器的一種工作方式,用于控制類的加載過程。類加載器是實作雙親委派機制的具體實體。

類加載器的主要任務是将類的位元組碼加載到記憶體中,并建立對應的類對象。當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去找。

  1. AppClassLoader去自己已經加載的類裡邊找,如果沒有向上委托
  2. ExtClassLoader去自己已經加載的類裡邊找,如果沒有向上委托。
  3. 引導類去自己加載的類裡邊找,如果沒有,
  4. 引導類嘗試加載,判斷這個類是不是應該由自己加載,判斷是不是JRE包路徑下的,如果是加載并且傳回,不是向下傳播。
  5. ExtClassLoader判斷這個類是不是ext包路徑下的,如果是加載并傳回,不是向下傳播。
  6. 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(它就打破了雙親委派機制,隻是自己加載,不會向上委托)。

上一篇: #77_edit

繼續閱讀