深入理解JVM類載入器

卡巴拉的樹發表於2017-11-30

在我的深入理解JVM類載入機制中,類載入器的部分我只談了一點點內容,這篇文章將深入瞭解Java中的類載入器是如何工作的。

類載入器

類載入的第一個階段就需要通過一個類的全限定名來獲取描述此類的二進位制位元組流,實現這個動作的模組就是類載入器。

類載入器雖然只是實現類的載入動作,但是在Java程式中的作用遠不止於此。在Java中一個類的唯一性不僅僅是看類本身,還要看它的載入器。通俗地說:比較兩個類是否相等,只有在兩個類時由同一個類載入器載入的前提下才有意義,否則,即使兩個類來源於同一個Class檔案,被同一個虛擬機器載入,只要類載入器不同,兩個類也不相等。

下面看周志明老師那本書上例舉的一段程式碼:

import java.io.IOException;
import java.io.InputStream;

public class ClassLoaderTest {

    public static void main(String[] args) throws Exception{
        ClassLoader myLoader = new ClassLoader() {
            @Override
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                try {
                    String fileName = name.substring(name.lastIndexOf(".") + 1)+".class";
                    InputStream is = getClass().getResourceAsStream(fileName);
                    if (is==null) {
                        return super.loadClass(name);
                    }
                    byte[] b =new byte[is.available()];
                    is.read(b);
                    return defineClass(name,b,0,b.length);
                } catch (IOException e) {
                    throw new ClassNotFoundException(name);
                }
            }
        };
        Object obj = myLoader.loadClass("ClassLoaderTest").newInstance();
        System.out.println(obj.getClass());  //class ClassLoaderTest
        System.out.println(obj instanceof  ClassLoaderTest);  //false
    }
}複製程式碼

註釋後列印了輸出內容,我們使用自己定義的類載入器去載入本身產生的class檔案,產生Class類,再例項化產生了obj物件。從列印的第一句看出,obj確實是ClassLoaderTest例項化的物件,但第二句返回了false,因為系統記憶體在兩個ClassLoaderTest類,一個是系統應用程式類載入器載入的,一個是我們自定義的類載入器載入的,雖然來自於同一個class檔案,但是卻是兩個類。

上面我們提到了應用程式載入器,自定義類載入器什麼的,別急,繼續往下看。

雙親委派模型

什麼是雙親委派模型想必也是面試中的常問考點。之前我們也提到過在JVM中,只存在兩種類載入器:

  • 啟動類載入器(Bootstrap ClassLoader): C++實現,虛擬機器的一部分
  • 使用者自定義類載入器(User-Defined Class Loader): Java語言實現,獨立於虛擬機器外部,並且都是繼承與java.lang.ClassLoader

一般來說,在討論類載入器時,我們會劃分的更細,我們可以看下圖:

類載入器
類載入器

下面的討論都將基於這幅圖。

絕大部分的Java程式都會使用到以下3種系統提供的類載入器。

  • 啟動類載入器(Bootstrap ClassLoader): 這個類將負責把<JAVA_HOME>\lib\目錄中的,或者-Xbootclasspath引數指定的目錄所指定的路徑中的,並且是虛擬機器識別的的類庫載入到虛擬機器記憶體中,如rt.jar,識別僅按照檔名識別,如果名字不符合,即使在這個目錄下,也不會被載入。啟動類載入器無法被java程式直接引,使用者如果在編寫自定義的類載入器時,如果需要把載入請求委託給引導類載入器,那麼直接用null代替即可。
  • 擴充套件類載入器(Extension ClassLoader): 這個類載入器由sun.misc.Launcher $ExtClassLoader實現,它負責載入<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。
  • 應用程式載入類(Application ClassLoader): 這個類載入器是由sun.misc.Launcher $App-ClassLoader實現。該載入器是由ClassLoader的getSystemClassLoader()方法返回,所以一般稱它為系統類載入器。一般它載入使用者類路徑(ClassPath)所指定的類庫,開發者一般直接使用這個類載入器,如果沒有定義自己的類載入器,那麼這個應用程式載入類就是程式中預設的類載入器。

我們來解釋下什麼是雙親委派模型?

雙親委派模型要求除了頂層的Bootstrap ClassLoader外,其它所有類載入器都要有自己的父類載入器。這裡的父子關係一般不會議繼承實現,而是通過組合實現。它的基本工作流程是:如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器完成,每個層次的類載入器都是如此,因此最後所有的請求都會傳遞到頂層的啟動類載入器中,只有當父載入器返回自己無法完成這個載入請求(即它的搜尋範圍內沒有找到所需要的類),子載入器才會嘗試去自己載入

使用雙親委派模型的好處呢?

使用雙親委派模型最直接的好處就是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係,例如jaba.lang.Object類,它存放在rt.jar中,無論哪一個類載入器要載入這個類,最終都要委派給最上層的boostrap ClassLoader,所以Object類在程式的各種類載入器環境中都是同一個類。假如沒有使用雙親委派模型,由各個類各自載入Object,那麼系統裡將會出現各種版本的Object類,導致整個系統的混亂。

下面我們從ClassLoader的原始碼來看看雙親委派模式:

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) {//沒載入過的情況
                long t0 = System.nanoTime();  
                try {
                    if (parent != null) {
                        //如果自定義的類載入器的parent不為null,就呼叫parent的loadClass進行載入類 
                        c = parent.loadClass(name, false);   
                    } else {
                        //否則就去找bootstrap ClassLoader
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }複製程式碼

從上面的程式碼來看,使用指定名稱載入類分為以下3步:

  1. 呼叫findLoadedClass(String)來檢查類是否已經被載入;
  2. 呼叫父類的loadClass方法,如果父類為空,就呼叫虛擬機器內建的引導類載入器載入;
  3. 呼叫findClass(String)來查詢該類。

因為loadClass封裝了雙親委派模型,所以在開發自己的類載入器時,Java標準提覆寫findClass()方法。

通常情況下,Java虛擬機器都是從檔案系統裡load一個class,但是有一些類不一定來自一個檔案,它們也可能來自別的源,比如網路,加密檔案等等,假設我們寫一個自己的類載入器,載入伺服器下載的class檔案。

示例使用程式碼:

ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();複製程式碼

在定義時,我們覆寫findClass方法:

class NetworkClassLoader extends ClassLoader {
         String host;
         int port;

         public Class findClass(String name) {
             byte[] b = loadClassData(name);
             return defineClass(name, b, 0, b.length);
         }

         private byte[] loadClassData(String name) {
             // load the class data from the connection
              . . .
         }
     }複製程式碼

這裡重要的還有個defineClass函式,用來把一組二進位制位元組轉換為Class的例項,轉換為Class後再交給後續的類載入過程解析。後續步驟就又回到深入理解JVM類載入機制中所描述的了。

相關文章