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

不管你是跟同事、同窗、上下級、同行、或者面試官討論技術問題的時候,很容易捲入JVM大型撕逼現場。爲了可以讓你們從大型撕逼現場中脫穎而出,最近我苦思冥想如何把知識點儘量呈現的容易理解,方便記憶。因而就開啓了這一系列文章的編寫。爲了讓JVM相關知識點可以造成一個體系,arthinking將編寫整理一系列的專題,以儘可能以圖片的方式描述相關知識點,而且最終把全部相關知識點串成了一張圖。持續更新中,歡迎你們閱讀。有任何錯落之處也請您高擡貴手幫忙指正,感謝!html

導讀:

  1. 類加載器是怎麼被建立出來的?
  2. 什麼是雙親委派機制?爲何要有這種機制?
  3. Class實例和類加載器到底是在Java Heap中,仍是在方法區中?

類加載器: 能夠實現經過一個類的全限定名稱來獲取描述此類的二進制字節流。實現這個動做的代碼模塊成爲」類加載器「。java

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

一、類加載器的加載流程

image-20200105144705999

如上圖爲類加載器的加載流程。面試

這裏簡單描述下:算法

1.一、啓動類加載器

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

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

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

能夠反編譯rt.jar文件查看詳細代碼:編程

image-20200105124613663

image-20200105131342939

在加載到Launcher類完成後,會對該類進行初始化,初始化的過程當中,會建立 ExtClassLoader 和 AppClassLoader,源碼以下:bootstrap

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.二、擴展類加載器

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

1.三、應用程序類加載器

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

引用程序類加載器也叫系統類加載器,能夠經過getSystemClassLoader方法獲得應用程序類加載器。

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

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

2.一、雙親委派機制原理

雙親委派模型在JDK1.2以後被引入,並普遍使用,這不是一個強制性的約束模型,二貨思Java設計者推薦給開發者的一種類加載器實現方式。咱們也能夠覆蓋對應的方式,實現本身的加載模型。

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

image-20200105170731274

也就是說:

  • 一個類加載器收到了類加載請求,不會本身馬上嘗試加載類,而是把請求委託給父加載器去完成,每一層都是如此,全部的家在請求最終都傳遞到最頂層的類加載器進行處理;
  • 若是父加載器不存在了,那麼嘗試判斷有沒有被啓動類加載器加載;
  • 若是的確沒有被夾在,則再本身嘗試加載。

問題:

  1. 爲何要有這麼複雜的雙親委派機制?
    1. 若是沒有這種機制,咱們就能夠篡改啓動類加載器中須要的類了,如,修本身編寫一個java.lang.Object用本身的類加載器進行加載,系統中就會存在多個Object類,這樣Java類型體系最基本的行爲也就沒法保證了。

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.三、類加載器和Class實例的題外話

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

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

以下圖:

image-20200105200625589

方法區裏面存儲着加載進來的類信息,方法區同時僱傭了兩類工種幫忙幹活:

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

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

三、其餘非雙親委派模型的案例

3.一、JDK 1.0遺留問題

在JDK1.0已經存在了ClassLoader類,可是當時尚未雙親委派機制,用戶爲了自定義類加載器,須要從新loadClass()方法,而咱們知道,在JDK1.2之後,loadClass裏面就是雙親委派機制的實現代碼,此時,要實現自定義類加載器,須要從新findClass()類便可。

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

3.二、線程上下文類加載器

爲何須要這個東西呢,咱們仍是從一個案例來講起。

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.三、模塊熱部署

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

References

Where are static methods and static variables stored in Java?

ClassLoader in Java

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

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

Chapter 5. Loading, Linking, and Initializing


本文爲arthinking基於相關技術資料和官方文檔撰寫而成,確保內容的準確性,若是你發現了有何錯漏之處,煩請高擡貴手幫忙指正,萬分感激。

你們能夠關注個人博客:itzhai.com 獲取更多文章,我將持續更新後端相關技術,涉及JVM、Java基礎、架構設計、網絡編程、數據結構、數據庫、算法、併發編程、分佈式系統等相關內容。

若是您以爲讀完本文有所收穫的話,能夠關注個人帳號,或者點贊的,您的支持就是我寫做的動力!關注個人公衆號,及時獲取最新的文章。


本文做者: arthinking

博客連接: www.itzhai.com/jvm/what-is…

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

版權聲明: BY-NC-SA許可協議:創做不易,如需轉載,請務必附加上博客連接,謝謝!


相關文章
相關標籤/搜索