淺談雙親委派模型

本文淺析了雙親委派的基本概念、實(shí)現(xiàn)原理、和自定義類加載器的正確姿勢。

對于更細(xì)致的加載loading過程、初始化initialization順序等問題,文中暫不涉及,后面整理筆記時有相應(yīng)的文章。

JDK版本:oracle java 1.8.0_102

基本概念

定義

雙親委派模型要求除了頂層的啟動類加載器外,其余的類加載器都應(yīng)當(dāng)有自己的父類加載器。

雙親委派模型的工作過程是:

  • 如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成。
  • 每一個層次的類加載器都是如此。因此,所有的加載請求最終都應(yīng)該傳送到頂層的啟動類加載器中。
  • 只有當(dāng)父加載器反饋?zhàn)约簾o法完成這個加載請求時(搜索范圍中沒有找到所需的類),子加載器才會嘗試自己去加載。

很多人對“雙親”一詞很困惑。這是翻譯的鍋,,,“雙親”只是“parents”的直譯,實(shí)際上并不表示漢語中的父母雙親,而是一代一代很多parent,即parents。

作用

對于任意一個類,都需要由加載它的類加載器和這個類本身一同確立其在虛擬機(jī)中的唯一性,每一個類加載器,都擁有一個獨(dú)立的類名稱空間。因此,使用雙親委派模型來組織類加載器之間的關(guān)系,有一個顯而易見的好處:類隨著它的類加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。

例如類java.lang.Object,它由啟動類加載器加載。雙親委派模型保證任何類加載器收到的對java.lang.Object的加載請求,最終都是委派給處于模型最頂端的啟動類加載器進(jìn)行加載,因此Object類在程序的各種類加載器環(huán)境中都是同一個類。

相反,如果沒有使用雙親委派模型,由各個類加載器自行去加載的話,如果用戶自己編寫了一個稱為java.lang.Object的類,并用自定義的類加載器加載,那系統(tǒng)中將會出現(xiàn)多個不同的Object類,Java類型體系中最基礎(chǔ)的行為也就無法保證,應(yīng)用程序也將會變得一片混亂。

結(jié)構(gòu)

系統(tǒng)提供的類加載器

在雙親委派模型的定義中提到了“啟動類加載器”。包括啟動類加載器,絕大部分Java程序都會使用到以下3種系統(tǒng)提供的類加載器:

  • 啟動類加載器(Bootstrap ClassLoader)

負(fù)責(zé)將存放在<JAVA_HOME>/lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且是虛擬機(jī)按照文件名識別的(如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被加載)類庫加載到虛擬機(jī)內(nèi)存中。

啟動類加載器無法被Java程序直接引用,用戶在編寫自定義類加載器時,如果需要把加載請求委派給引導(dǎo)類加載器,那直接使用null代替即可。

JDK中的常用類大都由啟動類加載器加載,如java.lang.String、java.util.List等。需要特別說明的是,啟動類Main class也由啟動類加載器加載。

  • 擴(kuò)展類加載器(Extension ClassLoader)

sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)。

負(fù)責(zé)加載<JAVA_HOME>/lib/ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫。

開發(fā)者可以直接使用擴(kuò)展類加載器。

猴子對自己電腦<JAVA_HOME>/lib/ext目錄下的jar包都非常陌生。看了幾個jar包,也沒找到常用的類;唯一有點(diǎn)印象的是jfxrt.jar,被用于JavaFX的開發(fā)之中。

  • 應(yīng)用程序類加載器(Application ClassLoader)

sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)。由于這個類加載器是ClassLoader.getSystemClassLoader()方法的返回值,所以一般也稱它為系統(tǒng)類加載器。

它負(fù)責(zé)加載用戶類路徑ClassPath上所指定的類庫,開發(fā)者可以直接使用這個類加載器。如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。

啟動類Main class、其他如工程中編寫的類、maven引用的類,都會被放置在類路徑下。Main class由啟動類加載器加載,其他類由應(yīng)用程序類加載器加載。

自定義的類加載器

JVM建議用戶將應(yīng)用程序類加載器作為自定義類加載器的父類加載器。則類加載的雙親委派模型如圖:

image.png

實(shí)現(xiàn)原理

實(shí)現(xiàn)雙親委派的代碼都集中在ClassLoader#loadClass()方法之中。將統(tǒng)計(jì)部分的代碼去掉之后,簡寫如下:

public abstract class ClassLoader {
    ...
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                ...
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    ...
                    c = findClass(name);
                    // do some stats
                    ...
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
    ...
}
  • 首先,檢查目標(biāo)類是否已在當(dāng)前類加載器的命名空間中加載(即,使用二元組<類加載器實(shí)例,全限定名>區(qū)分不同類)。
  • 如果沒有找到,則嘗試將請求委托給父類加載器(如果指定父類加載器為null,則將啟動類加載器作為父類加載器;如果沒有指定父類加載器,則將應(yīng)用程序類加載器作為父類加載器),最終所有類都會委托到啟動類加載器。
  • 如果父類加載器加載失敗,則自己加載。
  • 默認(rèn)resolve取false,不需要解析,直接返回。

自定義類加載器的正確姿勢

系統(tǒng)提供的3種類加載器分別負(fù)責(zé)各路徑下的Java類的加載。如果用戶希望自定義一個類加載器(如從網(wǎng)絡(luò)中讀取class字節(jié)流,以加載新的類),該如何做呢?

錯誤姿勢

先來看幾個類加載的錯誤姿勢。

再次提醒,以下這些錯誤姿勢一定不影響編譯,因?yàn)榧虞d行為發(fā)生在運(yùn)行期。

不定義類加載器

現(xiàn)在用戶自定義了一個sun.applet.Main類,但不定義類加載器:

package sun.applet;

/**
 * Created by monkeysayhi on 2017/12/20.
 */
public class Main {
  public Main() {
    System.out.println("constructed");
  }

  public static void main(String[] args) {
    System.out.println("recognized as sun.applet.Main in jdk," +
        " and there isn't any main method");
  }
}

為保持與后續(xù)實(shí)驗(yàn)的連貫性,這里沒有選擇常用的java.lang包下的類。原因見后。

將該類作為Main class啟動,會輸出什么呢?或許你以為會輸出12-13行聲明的字符串,現(xiàn)實(shí)卻總會啪啪啪撫摸我們的臉龐:

用法: appletviewer <options> url

其中, <options> 包括:
  -debug                  在 Java 調(diào)試器中啟動小應(yīng)用程序查看器
  -encoding <encoding>    指定 HTML 文件使用的字符編碼
  -J<runtime flag>        將參數(shù)傳遞到 java 解釋器

-J 選項(xiàng)是非標(biāo)準(zhǔn)選項(xiàng), 如有更改, 恕不另行通知。

不管這些東西從哪來的,總之不是我們定義的。

實(shí)際被選中的Main class是jdk中的sun.applet.Main類。如果沒有定義類加載器,則會使用默認(rèn)的類加載器(應(yīng)用程序類加載器)和默認(rèn)的類加載行為(ClassLoader#loadClass())。由雙親委派模型可知,最終將由啟動類加載器加載<JAVA_HOME>/lib/rt.jar中的sun.applet.Main,并執(zhí)行其main方法。

定義類加載器,但不委派

如何不委派呢?覆寫ClassLoader#loadClass():

當(dāng)然,還要覆寫ClassLoader#findClass()以支持自定義的類加載方式。

public class UnDelegationClassLoader extends ClassLoader {
  private String classpath;

  public UnDelegationClassLoader(String classpath) {
    super(null);
    this.classpath = classpath;
  }

  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    Class<?> clz = findLoadedClass(name);
    if (clz != null) {
      return clz;
    }

    // jdk 目前對"java."開頭的包增加了權(quán)限保護(hù),這些包我們?nèi)匀唤唤o jdk 加載
    if (name.startsWith("java.")) {
      return ClassLoader.getSystemClassLoader().loadClass(name);
    }
    return findClass(name);
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    InputStream is = null;
    try {
      String classFilePath = this.classpath + name.replace(".", "/") + ".class";
      is = new FileInputStream(classFilePath);
      byte[] buf = new byte[is.available()];
      is.read(buf);
      return defineClass(name, buf, 0, buf.length);
    } catch (IOException e) {
      throw new ClassNotFoundException(name);
    } finally {
      if (is != null) {
        try {
          is.close();
        } catch (IOException e) {
          throw new IOError(e);
        }
      }
    }
  }

  public static void main(String[] args)
      throws ClassNotFoundException, IllegalAccessException, InstantiationException,
      MalformedURLException {
    sun.applet.Main main1 = new sun.applet.Main();

    UnDelegationClassLoader cl = new UnDelegationClassLoader("java-study/target/classes/");
    String name = "sun.applet.Main";
    Class<?> clz = cl.loadClass(name);
    Object main2 = clz.newInstance();

    System.out.println("main1 class: " + main1.getClass());
    System.out.println("main2 class: " + main2.getClass());
    System.out.println("main1 classloader: " + main1.getClass().getClassLoader());
    System.out.println("main2 classloader: " + main2.getClass().getClassLoader());
  }
}

注意16-19行。由于jdk對"java."開頭的包增加了權(quán)限保護(hù),用戶無法使用示例中的ClassLoader#defineClass()方法;而所有類都是java.lang.Object類的子類,sout輸出時也要使用java.lang.System類等,所以我們又必須加載java.lang包下的類。因此,我們?nèi)匀粚⑦@些包委托給jdk加載。

同時,這也解釋了,為什么不能將常用的java.lang包下的類作為同名類測試對象。

示例先加載jdk中的sun.applet.Main類,實(shí)例化main1,再使用不進(jìn)行委派的自定義類加載器加載自定義的sun.applet.Main類,實(shí)例化main2。如果實(shí)例main2創(chuàng)建成功,則輸出“constructed”。之后,輸出main1、main2的類名和類加載器。

輸出:

constructed
main1 class: class sun.applet.Main
main2 class: class sun.applet.Main
main1 classloader: null
main2 classloader: com.msh.demo.classloading.loading.UnDelegationClassLoader@1d44bcfa

首先,1行說明實(shí)例main2創(chuàng)建成功了。2-3行表示main1、main2的全限定名確實(shí)相同。4-5行表示二者的類加載器不同main1的類使用啟動類加載器,main2的類使用自定義的類加載器

正確姿勢

一個符合規(guī)范的類加載器,應(yīng)當(dāng)僅覆寫ClassLoader#findClass(),以支持自定義的類加載方式。不建議覆寫ClassLoader#loadClass()(以使用默認(rèn)的類加載邏輯,即雙親委派模型);如果需要覆寫,則不應(yīng)該破壞雙親委派模型

public class DelegationClassLoader extends ClassLoader {
  private String classpath;

  public DelegationClassLoader(String classpath, ClassLoader parent) {
    super(parent);
    this.classpath = classpath;
  }

  @Override
  protected Class<?> findClass(String name) throws ClassNotFoundException {
    InputStream is = null;
    try {
      String classFilePath = this.classpath + name.replace(".", "/") + ".class";
      is = new FileInputStream(classFilePath);
      byte[] buf = new byte[is.available()];
      is.read(buf);
      return defineClass(name, buf, 0, buf.length);
    } catch (IOException e) {
      throw new ClassNotFoundException(name);
    } finally {
      if (is != null) {
        try {
          is.close();
        } catch (IOException e) {
          throw new IOError(e);
        }
      }
    }
  }

  public static void main(String[] args)
      throws ClassNotFoundException, IllegalAccessException, InstantiationException,
      MalformedURLException {
    sun.applet.Main main1 = new sun.applet.Main();

    DelegationClassLoader cl = new DelegationClassLoader("java-study/target/classes/",
        getSystemClassLoader());
    String name = "sun.applet.Main";
    Class<?> clz = cl.loadClass(name);
    Object main2 = clz.newInstance();

    System.out.println("main1 class: " + main1.getClass());
    System.out.println("main2 class: " + main2.getClass());
    System.out.println("main1 classloader: " + main1.getClass().getClassLoader());
    System.out.println("main2 classloader: " + main2.getClass().getClassLoader());
    ClassLoader itrCl = cl;
    while (itrCl != null) {
      System.out.println(itrCl);
      itrCl = itrCl.getParent();
    }
  }
}

因?yàn)樵谧远x類加載器上正確使用了雙親委派模型,上述代碼運(yùn)行后,不會出現(xiàn)相同全限定名的類被不同類加載器加載的問題,也就不會引起混亂了.

輸出:

main1 class: class sun.applet.Main
main2 class: class sun.applet.Main
main1 classloader: null
main2 classloader: null
com.msh.demo.classloading.loading.DelegationClassLoader@1d44bcfa
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@266474c2

在雙親委派模型下,運(yùn)行時中只存在啟動類加載器加載的sun.applet.Main類。

5-6行輸出了類加載器在雙親委派模型中的位置:最下層是自定義類加載器,然后逐層向上是應(yīng)用程序類加載器、擴(kuò)展類加載器,最上層是啟動類加載器(在擴(kuò)展類加載器中記為null)??膳c前面的結(jié)構(gòu)圖對照。

不過,實(shí)際情況中,覆寫ClassLoader#loadClass()是非常常見的。JNDI、OSGi等為了實(shí)現(xiàn)各自的需求,也在一定程度上破壞了雙親委派模型。


本文鏈接:

本文鏈接:淺談雙親委派模型
作者:猴子007
出處:https://monkeysayhi.github.io
本文基于 知識共享署名-相同方式共享 4.0 國際許可協(xié)議發(fā)布,歡迎轉(zhuǎn)載,演繹或用于商業(yè)目的,但是必須保留本文的署名及鏈接。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容