1. 程式人生 > >Java虛擬機器類載入器及雙親委派機制

Java虛擬機器類載入器及雙親委派機制

所謂的類載入器(Class Loader)就是載入Java類到Java虛擬機器中的,前面《面試官,不要再問我“Java虛擬機器類載入機制”了》中已經介紹了具體載入class檔案的機制。本篇文章我們重點介紹載入器和雙親委派機制。

類載入器

在JVM中有三類ClassLoader構成:啟動類(或根類)載入器(Bootstrap ClassLoader)、擴充套件類載入器(ExtClassLoader)、應用類載入器(AppClassLoader)。不同的類載入器負責不同區域的類的載入。

啟動類載入器:這個載入器不是一個Java類,而是由底層的c++實現,負責將存放在JAVA_HOME下lib目錄中的類庫,比如rt.jar。因此,啟動類載入器不屬於Java類庫,無法被Java程式直接引用,使用者在編寫自定義類載入器時,如果需要把載入請求委派給引導類載入器,那直接使用null代替即可。

擴充套件類載入器:由sun.misc.Launcher$ExtClassLoader實現,負責載入JAVA_HOME下lib\ext目錄下的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫,開發者可以直接使用擴充套件類載入器。

應用類載入器:由sun.misc.Launcher$AppClassLoader實現的。由於這個類載入器是ClassLoader中的getSystemClassLoader方法的返回值,所以也叫系統類載入器。它負責載入使用者類路徑上所指定的類庫,可以被直接使用。如果未自定義類載入器,預設為該類載入器。

可以通過這種方式列印載入路徑及相關jar:

System.out.println("boot:" + System.getProperty("sun.boot.class.path"));
System.out.println("ext:" + System.getProperty("java.ext.dirs"));
System.out.println("app:" + System.getProperty("java.class.path"));

在列印的日誌中,可以看到詳細的路徑以及路徑下面都包含了哪些類庫。由於列印內容較多,這裡就不展示了。

類載入器的初始化

除啟動類載入器外,擴充套件類載入器和應用類載入器都是通過類sun.misc.Launcher進行初始化,而Launcher類則由根類載入器進行載入。相關程式碼如下:

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        //初始化擴充套件類載入器,建構函式沒有入參,無法獲取啟動類載入器
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        //初始化應用類載入器,入參為擴充套件類載入器
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    // 設定上下文類載入器
    Thread.currentThread().setContextClassLoader(this.loader);
    
   //...
}

雙親委派模型

雙親委派模型:當一個類載入器接收到類載入請求時,會先請求其父類載入器載入,依次遞迴,當父類載入器無法找到該類時(根據類的全限定名稱),子類載入器才會嘗試去載入。

雙親委派中的父子關係一般不會以繼承的方式來實現,而都是使用組合的關係來複用父載入器的程式碼。

通過編寫測試程式碼,進行debug,可以發現雙親委派過程中不同類載入器之間的組合關係。

而這一過程借用一張時序圖來檢視會更加清晰。

ClassLoader#loadClass原始碼

ClassLoader類是一個抽象類,但卻沒有包含任何抽象方法。繼承ClassLoader類並重寫findClass方法便可實現自定義類載入器。但如果破壞上面所述的雙親委派模型來實現自定義類載入器,則需要繼承ClassLoader類並重寫loadClass方法和findClass方法。

ClassLoader類的部分原始碼如下:

protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
    //進行類載入操作時首先要加鎖,避免併發載入
    synchronized (getClassLoadingLock(name)) {
        //首先判斷指定類是否已經被載入過
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //如果當前類沒有被載入且父類載入器不為null,則請求父類載入器進行載入操作
                    c = parent.loadClass(name, false);
                } else {
                   //如果當前類沒有被載入且父類載入器為null,則請求根類載入器進行載入操作
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }

            if (c == null) {
                long t1 = System.nanoTime();
               //如果父類載入器載入失敗,則由當前類載入器進行載入,
                c = findClass(name);
                //進行一些統計操作
               // ...
            }
        }
        //初始化該類
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

上面程式碼中也提現了不同類載入器之間的層級及組合關係。

為什麼使用雙親委派模型

雙親委派模型是為了保證Java核心庫的型別安全。所有Java應用都至少需要引用java.lang.Object類,在執行時這個類需要被載入到Java虛擬機器中。如果該載入過程由自定義類載入器來完成,可能就會存在多個版本的java.lang.Object類,而且這些類之間是不相容的。

通過雙親委派模型,對於Java核心庫的類的載入工作由啟動類載入器來統一完成,保證了Java應用所使用的都是同一個版本的Java核心庫的類,是互相相容的。

上下文類載入器

子類載入器都保留了父類載入器的引用。但如果父類載入器載入的類需要訪問子類載入器載入的類該如何處理?最經典的場景就是JDBC的載入。

JDBC是Java制定的一套訪問資料庫的標準介面,它包含在Java基礎類庫中,由根類載入器載入。而各個資料庫廠商的實現類庫是作為第三方依賴引入使用的,這部分實現類庫是由應用類載入器進行載入的。

獲取Mysql連線的程式碼:

//載入驅動程式
Class.forName("com.mysql.jdbc.Driver");
//連線資料庫
Connection conn = DriverManager.getConnection(url, user, password);

DriverManager由啟動類載入器載入,它使用到的資料庫驅動(com.mysql.jdbc.Driver)是由應用類載入器載入的,這就是典型的由父類載入器載入的類需要訪問由子類載入器載入的類。

這一過程的實現,看DriverManager類的原始碼:

//建立資料庫連線底層方法
private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
    //獲取呼叫者的類載入器
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        //由啟動類載入器載入的類,該值為null,使用上下文類載入器
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    //...

    for(DriverInfo aDriver : registeredDrivers) {
        //使用上下文類載入器去載入驅動
        if(isDriverAllowed(aDriver.driver, callerCL)) {
            try {
                //載入成功,則進行連線
                Connection con = aDriver.driver.connect(url, info);
                //...
            } catch (SQLException ex) {
                if (reason == null) {
                    reason = ex;
                }
            }
        } 
        //...
    }
}

在上面的程式碼中留意改行程式碼:

callerCL = Thread.currentThread().getContextClassLoader();

這行程式碼從當前執行緒中獲取ContextClassLoader,而ContextClassLoader在哪裡設定呢?就是在上面的Launcher原始碼中設定的:

// 設定上下文類載入器
Thread.currentThread().setContextClassLoader(this.loader);

這樣一來,所謂的上下文類載入器本質上就是應用類載入器。因此,上下文類載入器只是為了解決類的逆向訪問提出來的一個概念,並不是一個全新的類載入器,本質上是應用類載入器。

自定義類載入器

自定義類載入器只需要繼承java.lang.ClassLoader類,然後重寫findClass(String name)方法即可,在方法中指明如何獲取類的位元組碼流。

如果要破壞雙親委派規範的話,還需重寫loadClass方法(雙親委派的具體邏輯實現)。但不建議這麼做。

public class ClassLoaderTest extends ClassLoader {

    private String classPath;

    public ClassLoaderTest(String classPath) {
        this.classPath = classPath;
    }

    /**
     * 編寫findClass方法的邏輯
     *
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 獲取類的class檔案位元組陣列
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            // 生成class物件
            return defineClass(name, classData, 0, classData.length);
        }
    }

    /**
     * 編寫獲取class檔案並轉換為位元組碼流的邏輯
     *
     * @param className
     * @return
     */
    private byte[] getClassData(String className) {
        // 讀取類檔案的位元組
        String path = classNameToPath(className);
        try {
            InputStream is = new FileInputStream(path);
            ByteArrayOutputStream stream = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int num = 0;
            // 讀取類檔案的位元組碼
            while ((num = is.read(buffer)) != -1) {
                stream.write(buffer, 0, num);
            }
            return stream.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 類檔案的完全路徑
     *
     * @param className
     * @return
     */
    private String classNameToPath(String className) {
        return classPath + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
    }

    public static void main(String[] args) {
        String classPath = "/Users/zzs/my/article/projects/java-stream/src/main/java/";
        ClassLoaderTest loader = new ClassLoaderTest(classPath);

        try {
            //載入指定的class檔案
            Class<?> object1 = loader.loadClass("com.secbro2.classload.SubClass");
            System.out.println(object1.newInstance().toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

列印結果:

SuperClass static init
SubClass static init
com.secbro2.classload.SubClass@5451c3a8

關於SuperClass和SubClass在上篇文章《面試官,不要再問我“Java虛擬機器類載入機制”了》已經貼過程式碼,這裡就不再貼出了。

通過上面的程式碼可以看出,主要重寫了findClass獲取class的路徑便實現了自定義的類載入器。

那麼,什麼場景會用到自定義類載入器呢?當JDK提供的類載入器實現無法滿足我們的需求時,才需要自己實現類載入器。比如,OSGi、程式碼熱部署等領域。

Java9類載入器修改

以上類載入器模型為Java8以前版本,在Java9中類載入器已經發生了變化。在這裡主要簡單介紹一下相關模型的變化,具體變化細節就不再這裡展開了。

java9中目錄的改變。

Java9中類載入器的改變。

在java9中,應用程式類載入器可以委託給平臺類載入器以及啟動類載入器;平臺類載入器可以委託給啟動類載入器和應用程式類載入器。

在java9中,啟動類載入器是由類庫和程式碼在虛擬機器中實現的。為了向後相容,在程式中仍然由null表示。例如,Object.class.getClassLoader()仍然返回null。但是,並不是所有的JavaSE平臺和JDK模組都由啟動類載入器載入。

舉幾個例子,啟動類載入器載入的模組是java.base,java.logging,java.prefs和java.desktop。其他JavaSE平臺和JDK模組由平臺類載入器和應用程式類載入器載入。

java9中不再支援用於指定引導類路徑,-Xbootclasspath和-Xbootclasspath/p選項以及系統屬性sun.boot.class.path。-Xbootclasspath/a選項仍然受支援,其值儲存在jdk.boot.class.path.append的系統屬性中。

java9不再支援擴充套件機制。但是,它將擴充套件類載入器保留在名為平臺類載入器的新名稱下。ClassLoader類包含一個名為getPlatformClassLoader()的靜態方法,該方法返回對平臺類載入器的引用。

小結

本篇文章主要基於java8介紹了Java虛擬機器類載入器及雙親委派機制,和Java8中的一些變化。其中,java9中更深層次的變化,大家可以進一步研究一下。該系列持續更新中,歡迎關注微信公眾號“程式新視界”。

原文連結:《Java虛擬機器類載入器及雙親委派機制》

《面試官》系列文章:

  • 《JVM之記憶體結構詳解》
  • 《面試官,不要再問我“Java GC垃圾回收機制”了》
  • 《面試官,Java8 JVM記憶體結構變了,永久代到元空間》
  • 《面試官,不要再問我“Java 垃圾收集器”了》
  • 《Java虛擬機器類載入器及雙親委派機制》


程式新視界:精彩和成長都不容錯過

相關推薦

Java虛擬機器載入雙親委派機制

所謂的類載入器(Class Loader)就是載入Java類到Java虛擬機器中的,前面《面試官,不要再問我“Java虛擬機器類載入機制”了》中已經介紹了具體載入class檔案的機制。本篇文章我們重點介紹載入器和雙親委派機制。 類載入器 在JVM中有三類ClassLoader構成:啟動類(或根類)載入器(Bo

JAVA載入雙親委派模型

一、類載入器 java中類載入器可以大致劃分為以下三類: 啟動類載入器:Bootstrap ClassLoader,負責載入存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath引數指定的路徑中的,並且能被虛擬機器識別的類庫(如rt.jar,所

Java虛擬機器-載入載入過程

類載入器 java.lang.ClassLoader類及其子類可以讓java程式碼動態地載入到JVM中。每一個類都有載入它的ClassLoader的引用。每一個類載入器類都有一個載入它的父類載入器,類載入器的頂端稱為啟動類載入器(Bootstrap Class

java 詳解載入雙親委派打破雙親委派

http://www.jb51.net/article/102920.htmhttps://www.cnblogs.com/wxd0108/p/6681618.html其實,雙親委派模型並不複雜。自定義類載入器也不難!隨便從網上搜一下就能搜出一大把結果,然後copy一下就能用

Java自定義載入雙親委派模型[轉]

其實,雙親委派模型並不複雜。自定義類載入器也不難!隨便從網上搜一下就能搜出一大把結果,然後copy一下就能用。但是,如果每次想自定義類載入器就必須搜一遍別人的文章,然後複製,這樣顯然不行。可是自定義類載入器又不經常用,時間久了容易忘記。相信你經常會記不太清loa

Java載入雙親委派機制

前言 之前詳細介紹了Java類的整個載入過程(類載入機制詳解)。雖然,篇幅較長,但是也不要被內容嚇到了,其實每個階段都可以用一句話來概括。 1)載入:查詢並載入類的二進位制位元組流資料。 2)驗證:保證被載入的類的正確性。 3)準備:為類的靜態變數分配記憶體,並設定預設初始值。 4)解析:把類中的符號引用轉換

深入JVM系列(三)之類載入載入雙親委派機制與常見問題

一.概述 定義:虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的java型別。類載入和連線的過程都是在執行期間完成的。 二.

java虛擬機器載入過程記憶體情況底層原始碼分析ClassLoader講解

讀書筆記加自我總結----------------------------------------------- 《瘋狂JAVAj講義》 《深入理解JAVA虛擬機器》第七章虛擬機器載入機制 《傳智播客Java底層公開課視訊》教學視訊 參考: 一、虛擬機器的類載入機制

java虛擬機器載入機制學習

1、什麼是類的載入 類的載入指的是將類的.class檔案中的二進位制資料讀入到記憶體中,將其放在執行時資料區的方法區內,然後在堆區建立一個java.lang.Class物件,用來封裝類在方法區內的資料結構。類的載入的最終產品是位於堆區中的Class物件,Class物件封裝了類在方法區內的資料結構

Java虛擬機器載入機制經典案例

package io.lgxkdream.test; class Father { static Father f = new Father(); static { System.out.println("father-1"); } { System.out.println("

java虛擬機器載入過程(精簡版)

java虛擬機器類載入過程步驟:     1.載入 將虛擬機器外部的二進位制位元組流儲存到方法區中: a.獲取此類二進位制流: 通過一個類的全限定名來獲取定義此類的二進位制流; b.資料結構轉化: 將位元組流所代表的靜態儲存結

jdk原始碼解析(七)——Java虛擬機器載入機制

前面我們講解了class檔案的格式,以及它是什麼樣的。那麼接下來需要了解它怎麼被載入到jvm中呢?jvm的載入機制又是怎麼一個過程呢?本文參考了《Java 虛擬機器規範(Java SE 7 版)》的第五章內容來詳細解釋一下 虛擬機器類載入機制:虛擬機器把描述類的資料從cla

Java載入雙親委派模型.md

0.類載入過程 一般來說,類載入分為3個過程,載入,連結和初始化。 1.載入階段,是Java將位元組碼資料從不同資料來源讀取到JVM中,並對映為JVM認可的Class物件,這裡的資料來源可能有Jar包,class檔案,甚至網路資料來源等。如果輸入資料不是ClassFile結構,則會丟

JAVA虛擬機器載入過程

什麼時候進行類載入 jvm虛擬機器規範沒有強制性的規定何時需要進行類的載入,但是如果遇到了以下幾種情況的指令則強制必須立即對類進行載入 new 建立物件的時候, getstatic 讀取靜態欄位的時候, putstatic 設定靜態欄位的時候, invokes

載入雙親委派打破雙親委派

一般的場景中使用Java預設的類載入器即可,但有時為了達到某種目的又不得不實現自己的類載入器,例如為了達到類庫的互相隔離,例如為了達到熱部署重載入功能。這時就需要自己定義類載入器,每個類載入器載入各自的類庫資源,以此達到資源隔離效果。在對資源的載入上可以沿用雙親委派機制,也可

Java虛擬機器載入時機

文章摘自:深入理解Java虛擬機器 第二版 周志明著  程式碼編譯的結果從本地機器碼轉變為位元組碼。 在Class檔案中描述的各種資訊,最終都需要載入到虛擬機器中之後才能執行和使用。虛擬機器把描述類的資料從class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以

Java載入雙親委派模型

1. 什麼是類載入機制? 程式碼編譯的結果從本地機器碼轉變成位元組碼,是儲存格式的一小步,卻是程式語言發展的一大步。 Java虛擬機器把描述類的資料從Class檔案載入進記憶體,並對資料進行校驗,轉換解析和初始化,最終形成可以唄虛擬機器直接使用的Java型別,這就是虛擬

十、JAVA多執行緒:JVM載入(自動載入雙親委託機制載入名稱空間、執行時包、的解除安裝等)

  Jvm提供了三大內建的類載入器,不同的類載入器負責將不同的類載入到記憶體之中 根載入器(Bootstrap ClassLoader) 是最頂層的載入器,是由C++編寫的,主要負責虛擬機器核心類庫的載入,如整個java.lang包,根載入器是獲取不到引用的,因此

Java 虛擬機器載入機制

看到這個題目,很多人會覺得我寫我的java程式碼,至於類,JVM愛怎麼載入就怎麼載入,博主有很長一段時間也是這麼認為的。隨著程式設計經驗的日積月累,越來越感覺到了解虛擬機器相關要領的重要性。閒話不多說,老規矩,先來一段程式碼吊吊胃口。public class SSClass{

Java虛擬機器載入的過程

1. 類載入的時機: 類從被載入到虛擬機器記憶體開始到卸載出記憶體,整個生命週期包括以下七個階段,其中載入,驗證,準備,初始化,解除安裝這5個階段的順序是確定的。 類在什麼情況下進行載入: 虛