淺談雙親委派模型

monkeysayhi發表於2019-02-25

本文淺析了雙親委派的基本概念、實現原理、和自定義類載入器的正確姿勢。

對於更細緻的載入loading過程、初始化initialization順序等問題,文中暫不涉及,後面整理筆記時有相應的文章。

JDK版本:oracle java 1.8.0_102

基本概念

定義

雙親委派模型要求除了頂層的啟動類載入器外,其餘的類載入器都應當有自己的父類載入器

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

  • 如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成。
  • 每一個層次的類載入器都是如此。因此,所有的載入請求最終都應該傳送到頂層的啟動類載入器中。
  • 只有當父載入器反饋自己無法完成這個載入請求時(搜尋範圍中沒有找到所需的類),子載入器才會嘗試自己去載入。

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

作用

對於任意一個類,都需要由載入它的類載入器和這個類本身一同確立其在虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。因此,使用雙親委派模型來組織類載入器之間的關係,有一個顯而易見的好處:類隨著它的類載入器一起具備了一種帶有優先順序的層次關係

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

相反,如果沒有使用雙親委派模型,由各個類載入器自行去載入的話,如果使用者自己編寫了一個稱為java.lang.Object的類,並用自定義的類載入器載入,那系統中將會出現多個不同的Object類,Java型別體系中最基礎的行為也就無法保證,應用程式也將會變得一片混亂。

結構

系統提供的類載入器

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

  • 啟動類載入器(Bootstrap ClassLoader)

負責將存放在<JAVA_HOME>/lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器按照檔名識別的(如rt.jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器記憶體中。

啟動類載入器無法被Java程式直接引用,使用者在編寫自定義類載入器時,如果需要把載入請求委派給引導類載入器,那直接使用null代替即可。

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

  • 擴充套件類載入器(Extension ClassLoader)

sun.misc.Launcher$ExtClassLoader實現。

負責載入<JAVA_HOME>/lib/ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。

開發者可以直接使用擴充套件類載入器。

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

  • 應用程式類載入器(Application ClassLoader)

sun.misc.Launcher$AppClassLoader實現。由於這個類載入器是ClassLoader.getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器。

它負責載入使用者類路徑ClassPath上所指定的類庫,開發者可以直接使用這個類載入器。如果應用程式中沒有自定義過自己的類載入器,一般情況下這個就是程式中預設的類載入器

啟動類Main class、其他如工程中編寫的類、maven引用的類,都會被放置在類路徑下。Main class由啟動類載入器載入,其他類由應用程式類載入器載入。

自定義的類載入器

JVM建議使用者將應用程式類載入器作為自定義類載入器的父類載入器。則類載入的雙親委派模型如圖:

image.png

實現原理

實現雙親委派的程式碼都集中在ClassLoader#loadClass()方法之中。將統計部分的程式碼去掉之後,簡寫如下:

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);
    }
    ...
}
複製程式碼
  • 首先,檢查目標類是否已在當前類載入器的名稱空間中載入(即,使用二元組<類載入器例項,全限定名>區分不同類)。
  • 如果沒有找到,則嘗試將請求委託給父類載入器(如果指定父類載入器為null,則將啟動類載入器作為父類載入器;如果沒有指定父類載入器,則將應用程式類載入器作為父類載入器),最終所有類都會委託到啟動類載入器。
  • 如果父類載入器載入失敗,則自己載入。
  • 預設resolve取false,不需要解析,直接返回。

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

系統提供的3種類載入器分別負責各路徑下的Java類的載入。如果使用者希望自定義一個類載入器(如從網路中讀取class位元組流,以載入新的類),該如何做呢?

錯誤姿勢

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

再次提醒,以下這些錯誤姿勢一定不影響編譯,因為載入行為發生在執行期。

不定義類載入器

現在使用者自定義了一個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");
  }
}
複製程式碼

為保持與後續實驗的連貫性,這裡沒有選擇常用的java.lang包下的類。原因見後。

將該類作為Main class啟動,會輸出什麼呢?或許你以為會輸出12-13行宣告的字串,現實卻總會啪啪啪撫摸我們的臉龐:

用法: appletviewer <options> url

其中, <options> 包括:
  -debug                  在 Java 偵錯程式中啟動小應用程式檢視器
  -encoding <encoding>    指定 HTML 檔案使用的字元編碼
  -J<runtime flag>        將引數傳遞到 java 直譯器

-J 選項是非標準選項, 如有更改, 恕不另行通知。
複製程式碼

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

實際被選中的Main class是jdk中的sun.applet.Main類。如果沒有定義類載入器,則會使用預設的類載入器(應用程式類載入器)和預設的類載入行為(ClassLoader#loadClass())。由雙親委派模型可知,最終將由啟動類載入器載入<JAVA_HOME>/lib/rt.jar中的sun.applet.Main,並執行其main方法

定義類載入器,但不委派

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

當然,還要覆寫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."開頭的包增加了許可權保護,這些包我們仍然交給 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."開頭的包增加了許可權保護,使用者無法使用示例中的ClassLoader#defineClass()方法;而所有類都是java.lang.Object類的子類,sout輸出時也要使用java.lang.System類等,所以我們又必須載入java.lang包下的類。因此,我們仍然將這些包委託給jdk載入。

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

示例先載入jdk中的sun.applet.Main類,例項化main1,再使用不進行委派的自定義類載入器載入自定義的sun.applet.Main類,例項化main2。如果例項main2建立成功,則輸出“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行說明例項main2建立成功了。2-3行表示main1、main2的全限定名確實相同。4-5行表示二者的類載入器不同main1的類使用啟動類載入器,main2的類使用自定義的類載入器

正確姿勢

一個符合規範的類載入器,應當僅覆寫ClassLoader#findClass(),以支援自定義的類載入方式。不建議覆寫ClassLoader#loadClass()(以使用預設的類載入邏輯,即雙親委派模型);如果需要覆寫,則不應該破壞雙親委派模型

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();
    }
  }
}
複製程式碼

因為在自定義類載入器上正確使用了雙親委派模型,上述程式碼執行後,不會出現相同全限定名的類被不同類載入器載入的問題,也就不會引起混亂了.

輸出:

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
複製程式碼

在雙親委派模型下,執行時中只存在啟動類載入器載入的sun.applet.Main類。

5-6行輸出了類載入器在雙親委派模型中的位置:最下層是自定義類載入器,然後逐層向上是應用程式類載入器、擴充套件類載入器,最上層是啟動類載入器(在擴充套件類載入器中記為null)。可與前面的結構圖對照。

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


本文連結:

本文連結:淺談雙親委派模型
作者:猴子007
出處:monkeysayhi.github.io
本文基於 知識共享署名-相同方式共享 4.0 國際許可協議釋出,歡迎轉載,演繹或用於商業目的,但是必須保留本文的署名及連結。

相關文章