一篇圖文徹底弄懂類加載器與雙親委派機制


 

導讀:

  1. 類加載器是怎么被創建出來的?
  2. 什么是雙親委派機制?為什么要有這種機制?
  3. Class實例和類加載器究竟是在Java Heap中,還是在方法區中?

類加載器: 可以實現通過一個類的全限定名稱來獲取描述此類的二進制字節流。實現這個動作的代碼模塊成為”類加載器“。

通過自定義類加載器可以實現各種有趣而強大的功能更:OSGi,熱部署,代碼加密等。

1、類加載器的加載流程

 

image-20200105144705999

 

如上圖為類加載器的加載流程。

這里簡單描述下:

1.1、啟動類加載器

**啟動類加載器:**系統啟動的時候,首先會通過由C++實現的啟動類加載器,加載<JAVA_HOME>/lib目錄下面的jar包,或者被-Xbootclasspath參數指定的路徑並且被虛擬機識別的文件名的jar包。把相關Class加載到方法區中。

這一步會加載關鍵的一個類:sun.misc.Launcher。這個類包含了兩個靜態內部類:

  • ExtClassLoader:擴展類加載器內部類,下面會講;
  • AppClassLoader:應用程序類加載器內部類,下面會講

可以反編譯rt.jar文件查看詳細代碼:

 

image-20200105124613663

 

 

image-20200105131342939

 

在加載到Launcher類完成后,會對該類進行初始化,初始化的過程中,會創建 ExtClassLoader 和 AppClassLoader,源碼如下:

public Launcher() {
    ExtClassLoader extClassLoader;
    try {
      extClassLoader = ExtClassLoader.getExtClassLoader();
    } catch (IOException iOException) {
      throw new InternalError("Could not create extension class loader", iOException);
    }
    try {
      this.loader = AppClassLoader.getAppClassLoader(extClassLoader);
    } catch (IOException iOException) {
      throw new InternalError("Could not create application class loader", iOException);
    }
    Thread.currentThread().setContextClassLoader(this.loader);
    ...

由於啟動類加載器是由C++實現的,所以在Java代碼里面是訪問不到啟動類加載器的,如果嘗試通過String.class.getClassLoader()獲取啟動類的引用,會返回null

問題:

  1. 啟動類加載器,擴展類加載器和應用類加載器都是又誰加載的?

    1. 啟動類加載器是JVM的內部實現,在JVM申請好內存之后,由JVM創建這個啟動類加載器
    2. 擴展類加載器和應用程序類加載器是由啟動類加載器加載進來的;
  2. 說說以下代碼輸出什么:

 public static void main(String[] args) {
     System.out.println("加載當前類的加載器:" + TestClassLoader.class.getClassLoader());
        System.out.println("加載應用程序類加載器的加載器"
                         + TestClassLoader.class.getClassLoader().getClass().getClassLoader());
        System.out.println("String類的啟動類加載器" + String.class.getClassLoader());
   }

1.2、擴展類加載器

如上圖,擴展類加載器負責加載<JAVA_HOME>/lib/ext目錄下或者被java.ext.dirs系統變量指定的路徑中的類。

1.3、應用程序類加載器

引用程序類加載器加載用戶類路徑下制定的類庫,如果應用程序沒有自定義過自己的類加載器,此類加載器就是默認的類加載器。

引用程序類加載器也叫系統類加載器,可以通過getSystemClassLoader方法得到應用程序類加載器。

注意,如上圖通過以上三個類加載器加載類到方法區之后,方法區中分別對應有各自的類信息存儲區。不同類加載器加載的同一個類文件不相等。

2、類加載器的雙親委派機制

2.1、雙親委派機制原理

雙親委派模型在JDK1.2之后被引入,並廣泛使用,這不是一個強制性的約束模型,二貨思Java設計者推薦給開發者的一種類加載器實現方式。我們也可以覆蓋對應的方式,實現自己的加載模型。

類加載器的雙親委派機制如下:

 

image-20200105170731274

 

也就是說:

  • 一個類加載器收到了類加載請求,不會自己立刻嘗試加載類,而是把請求委托給父加載器去完成,每一層都是如此,所有的家在請求最終都傳遞到最頂層的類加載器進行處理;
  • 如果父加載器不存在了,那么嘗試判斷有沒有被啟動類加載器加載;
  • 如果的確沒有被夾在,則再自己嘗試加載。

問題:

  1. 為什么要有這么復雜的雙親委派機制?
    1. 如果沒有這種機制,我們就可以篡改啟動類加載器中需要的類了,如,修自己編寫一個java.lang.Object用自己的類加載器進行加載,系統中就會存在多個Object類,這樣Java類型體系最基本的行為也就無法保證了。

2.2、雙親委派機制處理流程

JDK中默認的雙親委派處理流程是怎么的呢?接下來我們看看代碼,以下是java.lang.ClassLoader.loadClass()方法的實現:

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) {
                        c = parent.loadClass(name, false);
                    } else {
                        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;
        }
    }

轉成流程圖,即是:

 

image-20200105174045231

 

如山圖所以,總是先回嘗試讓父類加載器先加載,其次判斷啟動類加載器是否已經加載了,最后才嘗試從當前類加載器加載。轉換為更清晰的模型如下:

 

image-20200105195158889

 

雙親委派模型具有以下特點:

  • 可見性原則:
    • 應用類加載器是可以讀取到由擴展類加載器和啟動類加載器加載進來的Class的;
    • 擴展類加載器是可以讀取到由啟動類加載器加載進來的Class的;
  • 唯一性:
    • 類是唯一的,沒有重復的類;

2.3、類加載器和Class實例的題外話

啟動類加載器,擴展類加載器,應用程序類加載器,他們分別管理者各自方法區里的一個區塊。

根據上一篇文章我們知道,方法區里面主要存儲的是類的運行時數據結構,這個類的在方法區中的各種數據結構信息通過類的Class實例進行訪問。

如下圖:

 

image-20200105200625589

 

方法區里面存儲着加載進來的類信息,方法區同時雇佣了兩類工種幫忙干活:

  • **類加載器:**負責管理各個存儲區的類信息,如加載和卸載類信息;
  • **Class實例:**負責對接外部需求,如果外部有人想查看里面的類信息,則需要通過Class實例來獲取;

另外,方法區里面,啟動類加載器類信息對擴展兩類加載器類信息可見,而前面兩者的類信息又對應用程序類加載器類信息可見。

3、其他非雙親委派模型的案例

3.1、JDK 1.0遺留問題

在JDK1.0已經存在了ClassLoader類,但是當時還沒有雙親委派機制,用戶為了自定義類加載器,需要重新loadClass()方法,而我們知道,在JDK1.2以后,loadClass里面就是雙親委派機制的實現代碼,此時,要實現自定義類加載器,需要重新findClass()類即可。

如果重新了loadClass()方法,也就意味着不再遵循雙親委派模型了。

3.2、線程上下文類加載器

為什么需要這個東西呢,我們還是從一個案例來說起。

Tomcat中的類加載器

我們知道Tomcat目錄結構中有以下目錄:

  • /common/: 該目錄下的類庫可被Tomcat和所有的WebApp共同使用;

  • /server/: 該目錄下的類庫可被Tomcat使用,但對所有的WebApp不可見;

  • /shared/: 該目錄下的類庫可被所有的WebApp共同使用,但對Tomcat自己不可見;

另外Web應用程序還有自身的類庫,放在/WebApp/WEB-INF目錄中:這里面的類庫僅僅可以被此Web應用程序使用,對Tomcat和其他Web應用程序都不可見。 為了實現以上各個目錄的類庫可見性效果,Tomat提供了如下的自定義類加載器:

 

image-20200105205509075

 

現在如下場景:

我們發現Tomcat下面有若干個webapp,每個webapp都用到了spring,於是我們把spring的jar包放到了shared目錄中。

於是問題出現了:由於spring的jar包是由Shared類加載器加載的,假設我們要使用SpringContext的getBean方法,獲取webapp中的Bean,如果是按照雙親委派模型,就會有問題了,因為webapp中的Java類是對SharedClassLoader不可見的:

 

image-20200105213630571

 

Spring中的線程上下文類加載器

為了解決這個問題,Spring使用了線程上下文類加載器,即從ThreadLocal中獲取到當前線程的上下文類加載器,來加載所有的類庫和類。

關於Spring初始化源碼相關解讀,參考這篇文章:Spring IoC原理剖析

Spring中的bean類加載器

ApplicationContext中有一個beanClassLoader字段,這個是bean的類加載器,在prepareBeanFactory()方法中做了初始化:

beanFactory.setBeanClassLoader(getClassLoader());

getClassLoader方法如下:

@Override
    @Nullable
    public ClassLoader getClassLoader() {
        return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader());
    }

ClassUtils.getDefaultClassLoader()方法:

@Nullable
    public static ClassLoader getDefaultClassLoader() {
        ClassLoader cl = null;
        try {
            cl = Thread.currentThread().getContextClassLoader();
        }
        catch (Throwable ex) {
            // Cannot access thread context ClassLoader - falling back...
        }
        if (cl == null) {
            // No thread context class loader -> use class loader of this class.
            cl = ClassUtils.class.getClassLoader();
            if (cl == null) {
                // getClassLoader() returning null indicates the bootstrap ClassLoader
                try {
                    cl = ClassLoader.getSystemClassLoader();
                }
                catch (Throwable ex) {
                    // Cannot access system ClassLoader - oh well, maybe the caller can live with null...
                }
            }
        }
        return cl;
    }

可以發現,這里最終取了當前線程上下文中的ClassLoader。

加載Bean

我們來看看Spring加載Class的代碼。這里我們直接找到實例化Singletons的方法跟進去找需要關注的代碼:

我們發現在加載Bean Class的時候調用了這個方法:

AbstractBeanFactory:

ClassLoader beanClassLoader = getBeanClassLoader();

也就是用到了ApplicationContext中的beanClassLoader,線程上下文類加載器來加載Bean Class實例。

總結

Spring作為一個第三方類庫,可能被任何的ClassLoader加載,所以最靈活的方式是直接使用上下文類加載器。

3.3、模塊熱部署

主要是類似OSGi這類的模塊化熱部署技術。在OSGi中不再是雙親委派模型中的樹狀結構,而是更復雜的網狀結構。

References

Where are static methods and static variables stored in Java?

ClassLoader in Java

真正理解線程上下文類加載器(多案例分析)

《深入理解Java虛擬機-JVM高級特性與最佳實踐》

Chapter 5. Loading, Linking, and Initializing



 

來源: 掘金 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM