1. 程式人生 > 實用技巧 >JVM初探(四):類載入器

JVM初探(四):類載入器

一、概述

虛擬機器設計團隊把類載入階段中的“通過一個類的全限定名來獲取描述此類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需要的類。實現這個動作的程式碼模組稱為類載入器。

對於類載入,我們關注兩個方面的問題:

  • JVM定義的三個類載入器(Bootstrap,Extension,System)
  • 雙親委派模型(機制,JDBC違背案例)

二、類載入器

JVM使用以下三種類型的類載入器:

  • 啟動類(Bootstrap)類載入器

    這個類存放在<JAVA_HOME>\lib目錄中,無法被Java程式直接引用;

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

    這個類載入器負責載入<JAVA_HOME>\lib\ext目錄中的所有類庫,開發者可以直接使用;

  • 引用程式(Application)類載入器

    這個類載入器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱它為系統類載入器,負責載入使用者類路徑上所指定的類庫。開發者可以直接使用。

從java虛擬機器的角度來說,一種是啟動類載入器,是JVM的一部分;另一種是其他類載入器,獨立於JVM,全部繼承自抽象類java.lang.ClassLoader。只有其他類載入器程式設計師才能自己使用。

三、雙親委派模型

1.什麼是雙親委派模型

JVM在載入類時預設採用的是雙親委派機制。

通俗的講,就是某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父類載入器,然後父類載入器再委託給它的父類......以此類推,因此所有的載入請求最終都應該傳送到頂層的啟動類載入器中。

只有父類載入器無法完成載入請求時,才會使用子類載入器去載入

雙親委派模型的一個重要作用是為了保證類載入過程的安全性

  • 假設有一個開發者自己編寫了一個名為 java.lang.Object的類,想借此欺騙JVM。現在他要使用自定義 ClassLoader來載入自己編寫的 java.lang.Object類。
  • 但是無論哪一個類載入器要載入這個類,最終都會委派給啟動類載入器進行載入,啟動類載入器在其搜尋範圍內可以搜尋到的只有rt.jar
    中的java.lang.Object
  • 這樣就確保了Object類的唯一性,如果是String也是同理。

也因此,java判斷類是否是同一個類,就是通過判斷類的ClassLoader.class來判斷的,舉個簡單的例子:只要不是一個爹媽生出來的,哪怕長的一模一樣那也不是親兄弟。

2.類的載入順序

我們開啟ClassLoader類,找到loadClass()方法,可以在註釋上看到如何按順序搜尋類的:

*   Invoke {@link #findLoadedClass(String)} to check if the class
*   has already been loaded.  </p></li>
*
*   Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
*   on the parent class loader.  If the parent is <tt>null</tt> the class
*   loader built-in to the virtual machine is used, instead.  </p></li>
*
*   Invoke the {@link #findClass(String)} method to find the
*   class.

即:

  • 使用findLoadedClass()方法檢查類是否已經載入;
  • 呼叫父類載入器的loaderCalss()方法。如果父類是null,就直接呼叫虛擬機器內建的Bootstrap載入;
  • 如果父類無法載入,使用自己findClass()載入。

我們再看一下loadClass()方法的原始碼,更直觀的展示了雙親委派模型下是如何載入類的:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException {
    //執行緒安全,保證只有一個執行緒能對類初始化
    synchronized (getClassLoadingLock(name)) {
        //1.檢查類是否已經被載入
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //2.如果沒有被載入,判斷是否存在父類載入器
                if (parent != null) {
                    //3.有就遞迴委派給父類載入器載入
                    c = parent.loadClass(name, false);
                } else {
                    //4.否則就使用啟動類載入器
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果到了啟動類載入器還找不到類,則丟擲ClassNotFoundException
            }
		
            //如果父類載入器無法載入,就使用自身的findClass載入
            if (c == null) {
                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;
    }
}

這裡就是為什麼JDK在我們自定義類載入器的時候不推薦重寫loadClass()只讓我們重寫findClass()方法的原因了,因為如果重寫了方法,卻不遵守雙親委派模型,就有可能導致不可預料的後果。

當然,不推薦歸不推薦,如果有必要的話仍然可以重寫loadClass()方法,比如JDBC、JNDI就打破了雙親委派模型

3.三種類載入器的關係

我們執行一下以下程式碼:

public static void main( String[] args ) {
    System.out.println("應用程式載入器:"+ClassLoader.getSystemClassLoader());
    System.out.println("應用程式載入器的父類:"+ClassLoader.getSystemClassLoader().getParent());
    System.out.println("擴充套件載入器的父類:"+ClassLoader.getSystemClassLoader().getParent().getParent());
}

//輸出結果
應用程式載入器:sun.misc.Launcher$AppClassLoader@18b4aac2
應用程式載入器的父類:sun.misc.Launcher$ExtClassLoader@29453f44
擴充套件載入器的父類:null

由於啟動類載入器是jvm內建的,我們無法直接呼叫,所以返回值是null。