1. 程式人生 > >類載入器中的雙親委派模型詳解

類載入器中的雙親委派模型詳解

本文首發於個人網站,如需轉載請註明來源:類載入器中的雙親委派模型,看這篇就夠了

在上一篇文章中,我們梳理了類載入器的基本概念:類的生命週期、類載入器的作用、類的載入和解除安裝的時機等等,這篇文章我們接著前文繼續複習類載入器的知識,主要包括:JVM中有哪些類載入器?它們之間是什麼關係?什麼是雙親委派機制?

雙親委派模型

四種類載入器

從JVM的角度看,類載入器主要有兩類:Bootstrap ClassLoader和其他類載入,Bootstrap ClassLoader是C++語言實現,是虛擬機器自身的一部分;其他類載入器都是Java語言實現,不屬於虛擬機器,全部繼承自抽象類java.lang.ClassLoader。

從Java開發者的角度看,需要了解類載入器的雙親委派模型,如下圖所示:

  • Bootstrap ClassLoader:啟動類載入器,這個類載入器將負責存放在

  • Extension ClassLoader:擴充套件類載入器,這個類載入器由sun.misc.Launcher$ExtClassLoader實現,它負責載入

  • Application ClassLoader:應用程式類載入器,這個類載入器由sun.misc.Launcher$AppClassLoader實現,它負責載入使用者CLASSPATH環境變數指定的路徑中的所有類庫。如果應用程式中沒有自定義過自己的類載入器,這個就是一個Java程式中預設的類載入器。

  • 使用者自定義的類載入器:使用者在需要的情況下,可以實現自己的自定義類載入器,一般而言,在以下幾種情況下需要自定義類載入器:(1)隔離載入類。某些框架為了實現中介軟體和應用程式的模組的隔離,就需要中介軟體和應用程式使用不同的類載入器;(2)修改類載入的方式。類載入的雙親委派模型並不是強制的,使用者可以根據需要在某個時間點動態載入類;(3)擴充套件類載入源,例如從資料庫、網路進行類載入;(4)防止原始碼洩露。Java程式碼很容易被反編譯和篡改,為了防止原始碼洩露,可以對類的位元組碼檔案進行加密,並編寫自定義的類載入器來載入自己的應用程式的類。

例子1:不同的類載入器

在下面的程式碼中,java.util.HashMap是rt.jar包中的類,因此它的類載入器是null,DNSNameService類是放在ext目錄下的jar包中的類,因此它的類載入器是ExtClassLoader;MyClassLoaderTest的類載入器就是應用類載入器。

import java.util.HashMap;

import sun.net.spi.nameservice.dns.DNSNameService;

public class MyClassLoaderTest {

    public static void main(String[] args) {

        System.out.println("class loader for HashMap: " + HashMap.class.getClassLoader());
        System.out.println(
            "class loader for DNSNameService: " + DNSNameService.class.getClassLoader());
        System.out.println("class loader for this class: " + MyClassLoaderTest.class.getClassLoader());
        System.out.println("class loader for Blob class: " + com.mysql.jdbc.Blob.class.getClassLoader());

    }
}

執行上述程式碼的接入過下圖所示:

例子2:不同類載入器管理的檔案路徑

通過下面的這個程式,可以看到,每個類載入器負責的jar檔案路徑都不一樣:

public class JVMClassLoader {

    public static void main(String[] args) {
        System.out.println("引導類載入器載入路徑:" + System.getProperty("sun.boot.class.path"));
        System.out.println("擴充套件類載入器載入路徑:" + System.getProperty("java.ext.dirs"));
        System.out.println("系統類載入器載入路徑:" + System.getProperty("java.class.path"));
    }
}

例子3:Arthas中的classloader命令

Arthas中提供了classloader命令,可以用來檢視當前應用中的類載入器相關的統計資訊,如下圖所示,

  1. 輸入classloader後展示的表格彙總了當前應用的類載入器、每個類載入器的例項個數、每個類載入器載入的類的個數。
  2. 輸入classloader -t後,展示了當前應用中類載入器的層次結構,可以看出,BootStrap ClassLoader確實在類載入器體系的頂層,接下來是擴充套件類載入器,再然後是應用類載入器,這裡還有一個ArthasClassLoader,是Arthas自己實現的一個自定義類載入器。

雙親委派模型的工作過程

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

使用雙親委派模型來組織類載入器之間的關係,有一個顯而易見的好處就是Java類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都是同一個類。相反,如果沒有使用雙親委派模型,由各個類載入器自行去載入的話,如果使用者自己編寫了一個稱為java.lang.Object的類,並放在程式的Class Path中,那系統中將會出現多個不同的Object類,Java型別體系中最基礎的行為也就無法保證,應用程式也將會變得一片混亂。

雙親委派模型的實現非常簡單,實現雙親委派的程式碼在java.lang.ClassLoader的loadClass()方法之中,如下面的程式碼所示:

    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) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 如果父類載入器丟擲ClassNotFoundException,
                    // 說明父類載入器無法完成載入請求
                }

                if (c == null) {
                    // 在父類載入器無法載入的時候,再呼叫本類的findClass方法進行類載入請求
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    // 當前類載入器是該類的define class loader
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

破壞雙親委派模型

執行緒上下文載入器

如上所述,雙親委派模型很好得解決了各個類載入器的基礎類的統一問題(越基礎的類由越上層的載入器進行載入),如果基礎類又要回呼叫戶的類該怎麼辦?一個非常經典的例子就是SQL的驅動管理類——java.sql.DriverManager。

java.sql.DriverManager是Java的標準服務,該類放在rt.jar中,因此是由啟動類載入器載入的,但是在應用啟動的時候,該驅動類管理是需要載入由不同資料庫廠商實現的驅動,但是啟動類載入器找不到這些具體的實現類,為了解決這個問題,Java設計團隊提供了一個不太優雅的設計:執行緒上下文載入器(Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立執行緒時候它還沒有被設定,就會從父執行緒中繼承一個,如果再應用程式的全域性範圍都沒有設定過的話,那這個類載入器就是應用程式類載入器。

有了執行緒上下文載入器,就可以解決上面的問題——父類載入器需要請求子類載入器完成類載入的動作,這種行為實際上就是打破了雙親委派的載入規則。

原始碼分析

接下來,我們以java.sql.DriverManager為例,看下執行緒上下文載入器的用法,在java.sql.DriverManager類的下面這個靜態塊中,是JDBC驅動載入的入口。

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

順著loadInitialDrivers()方法往下看,使用執行緒上下文載入器的地方在ServiceLoader.load裡

    private static void loadInitialDrivers() {
                // ……省去別的程式碼
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                            
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
              
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
      //…… 省去別的程式碼

ServiceLoader.load方法的程式碼如下,JDBC的sqlDriverManager就是這裡獲得的上下文載入器來驅動使用者程式碼載入指定的類的。

    public static <S> ServiceLoader<S> load(Class<S> service) {
          // 獲取當前執行緒中的上下文類載入器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

那麼這個上下文載入器是什麼時候設定進去的呢?前面我們提到了:

這個類載入器可以通過java.lang.Thread類的setContextClassLoader()方法進行設定,如果建立執行緒時候它還沒有被設定,就會從父執行緒中繼承一個,如果再應用程式的全域性範圍都沒有設定過的話,那這個類載入器就是應用程式類載入器。

看下setContextClassLoader()方法別誰呼叫了,最終我們在Launcher中找到了如下程式碼:

public class 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);
        //……省去別的程式碼
    }
}

總結

這篇文章我們複習了類載入器的雙親委派模型、雙親委派模型的工作過程,以及打破雙親委派模型的必要性和原始碼分析。在第一部分的結尾,我們還演示了Arthas中關於類載入器的命令的用法,在實際排查問題時可以考慮使用。

參考資料

  1. https://www.journaldev.com/349/java-classloader#java-classloader-hierarchy
  2. https://www.cnblogs.com/joemsu/p/9310226.html
  3. 《深入理解Java虛擬機器》
  4. 《碼出高效Java開發手冊》

本號專注於後端技術、JVM問題排查和優化、Java面試題、個人成長和自我管理等主題,為讀者提供一線開發者的工作和成長經驗,期待你能在這裡有所收穫。

相關推薦

JVM載入機制(二)載入雙親委派模型

1、通過一個類的全限定名(包名與類名)來獲取定義此類的二進位制位元組流(Class檔案)。而獲取的方式,可以通過jar包、war包、網路中獲取、JSP檔案生成等方式。 2、將這個位元組流所代表

JAVA載入雙親委派模型

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

JVM載入雙親委派模型(二)

(7)URLClassLoader類 前面說到,ClassLoader這個頂級父類只是定義好了雙親委派模型的工作機制;但是ClassLoader是個抽象類,無法直接建立物件,所以需要由繼承它的子類完成建立物件的任務。子類需要自己實現findClass方法,並且在例項化時指定parent屬性的值

JVM載入雙親委派模型(一)

(1)動態載入       類載入器是 Java 語言的一個創新,也是 Java 語言流行的重要原因之一。它使得 Java 類可以被動態載入到 Java 虛擬機器中並執行。類載入器從 JDK 1.0 就出現了,最初是為了滿足 Java Applet 的需要

(二)載入雙親委派模型

類載入機制的第一個階段載入做的工作有: 1、通過一個類的全限定名(包名與類名)來獲取定義此類的二進位制位元組流(Class檔案)。而獲取的方式,可以通過jar包、war包、網路中獲取、JSP檔案生成等方式。 2、將這個位元組流所代表的靜態儲存結構轉化為方法區的執行時資

Java載入雙親委派模型.md

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

jvm:載入雙親委派模型

兩個類相等需要類本身相等,並且使用同一個類載入器進行載入。這是因為每一個類載入器都擁有一個獨立的類名稱空間。 這裡的相等,包括類的 Class 物件的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回結果為 true,也包括使用 ins

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

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

Java載入雙親委派模型

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

Java 載入(ClassLoader)/雙親委派模型

ClassLoader類載入器 Class類描述的是整個類的資訊,在Class類中提供的forName()方法,這個方法根據ClassPath配置的路徑進行類的載入,如果說現在你的類的載入路徑可能是網路、檔案,這個時候就必須實現類載入器,也就是ClassLoa

檔案的結構、JVM 的載入過程、載入機制、載入雙親委派模型

# 一、類檔案的結構 我們都知道,各種不同平臺的虛擬機器,都支援 “位元組碼 Byte Code” 這種程式儲存格式,這構成了 Java 平臺無關性的基石。甚至現在平臺無關性也開始演變出 “語言無關性” ,就是其他語言也可以執行在 Java 虛擬機器之上,比如現在的 Kotlin、Scala 等。

載入雙親委派模型

本文首發於個人網站,如需轉載請註明來源:類載入器中的雙親委派模型,看這篇就夠了 在上一篇文章中,我們梳理了類載入器的基本概念:類的生命週期、類載入器的作用、類的載入和解除安裝的時機等等,這篇文章我們接著前文繼續複習類載入器的知識,主要包括:JVM中有哪些類載入器?它們之間是什麼關係?什麼是雙親委派機制?

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

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

JVM總括四-載入過程、雙親委派模型、物件例項化

JVM總括四-類載入過程、雙親委派模型、物件例項化 一、 類載入過程 一定要注意每個過程執行的內容!!!!!! 1、Load:   將編譯後的.class檔案以二進位制流的方式載入到JVM記憶體中,並轉化為特定的資料結構,用到的就是classLoad二類載入器。這個過程中校驗cafe babe

JVM總括四-載入過程、雙親委派模型、物件例項化過程 JVM思考-init和clinit區別

JVM總括四-類載入過程、雙親委派模型、物件例項化過程 目錄:JVM總括:目錄 一、 類載入過程 類載入過程就是將.class檔案轉化為Class物件,類例項化的過程,(User user = new User(); 這個過程是物件例項化的過程); 一個.class檔案只有一個Class物件(位元

JVM思考-init和clinit區別 JVM總括四-載入過程、雙親委派模型、物件例項化過程

JVM思考-init和clinit區別 目錄:JVM總括:目錄 clinit和init的區別其實也就是Class物件初始化物件初始化的區別,詳情看我上一篇部落格:  JVM總括四-類載入過程、雙親委派模型、物件例項化過程 一、init和clinit方法執行時機不同   init是物件構

JVM總括四-載入過程、雙親委派模型、物件例項化過程

JVM總括四-類載入過程、雙親委派模型、物件例項化過程 目錄:JVM總括:目錄 一、 類載入過程 類載入過程就是將.class檔案轉化為Class物件, 類例項化 的過程 ,(User user = new User(); 這個過程是 物件例項化 的

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

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

Tomcat載入破壞雙親委派

轉載:https://blog.csdn.net/qq_38182963/article/details/78660779 http://www.cnblogs.com/aspirant/p/8991830.html http://www.cnblogs.com/xing901022/p/4574961.

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

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