溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java類加載機制原理是什么

發(fā)布時間:2021-08-24 10:54:33 來源:億速云 閱讀:147 作者:chen 欄目:大數(shù)據(jù)

本篇內(nèi)容介紹了“Java類加載機制原理是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!

前言

在具體介紹類加載機制之前,我們先來看下網(wǎng)上關于理解類加載機制的經(jīng)典題目:

public class Singleton {
    private static Singleton singleton = new Singleton();
    public static int counter1;
    public static int counter2 = 0;
    
    private Singleton() {
        counter1++;
        counter2++;
    }
    
    public static Singleton getSingleton() {
        return singleton;
    }
}
// 打印靜態(tài)屬性的值
public class TestSingleton{
    public static void main(String[] args) {
        Singleton singleton = Singleton.getSingleton();
        System.out.println("counter1=" + singleton.counter1);
        System.out.println("counter2=" + singleton.counter2);
    }
}
// 輸出結果:
>>> counter1=1
>>> counter2=0

關于為什么counter2=0,這里就不具體解釋了,只是想說下它考核了那幾個點:

  1. 類加載過程的5個階段先后順序:準備階段在初始化之前

  2. 準備階段和初始化階段各自做的事情

  3. 靜態(tài)初始化的細節(jié):先后順序

什么是類的加載

言歸正傳,我們先從類加載的定義說起,一句話概述,

虛擬機將class文件中的二進制數(shù)據(jù)流加載到JVM運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),并進行驗證、準備解析初始化等動作后,在內(nèi)存中創(chuàng)建java.lang.class對象,作為對方法區(qū)中該類數(shù)據(jù)結構的訪問入口。

這里有幾點要解釋下,class文件是指符合class文件格式的二進制數(shù)據(jù)流,也就是我們常說的字節(jié)碼文件,它是我們與JVM約定的格式協(xié)議,只要是符合class文件格式的二進制流,都可被JVM加載,這也是JVM跨平臺的基礎;另外,java.lang.class對象只是說在內(nèi)存創(chuàng)建,并沒有明確規(guī)定是否在Java堆中,對于Hotspot虛擬機,是存放在方法區(qū)的。

加載方式

類的加載方式分為兩種:隱式加載和顯式加載。

隱式加載

實際就是不用我們代碼主動聲明,而是JVM在適當?shù)臅r機自動加載類。比如主動引用某個類時,會自動觸發(fā)類加載和初始化階段。

顯式加載

則通常是指通過代碼的方式顯式加載指定類,常見以下幾種:

通過Class.forName()加載指定類。對于forName(String className)方法,默認會執(zhí)行靜態(tài)初始化,但如果使用另一個重載函數(shù)forName(String name, boolean initialize, ClassLoader loader),實際上是可以通過initialize來控制是否執(zhí)行靜態(tài)初始化

通過ClassLoader.loadClass()加載指定類,這種方式僅僅是將.class加載到JVM,并不會執(zhí)行靜態(tài)初始化塊,這個等后面談到類加載器的職責時會再強調(diào)這一點

關于Class.forName()是否執(zhí)行靜態(tài)初始化,通過源碼就能一目了然:

public static Class<?> forName(String className)	// 執(zhí)行初始化,因為initialize為true
            throws ClassNotFoundException {
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
...
public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)  // 可控的,通過initialize來指定初始化與否
    throws ClassNotFoundException
{
    ...
    return forName0(name, initialize, loader, caller);
}

加載時機

類加載的第一個階段——加載階段具體什么時候開始,虛擬機規(guī)范并未指明,由具體的虛擬機實現(xiàn)決定,可分為預加載和運行時加載兩種時機:

  1. 預加載:對于JDK中的常用基礎庫——JAVA_HOME/lib下的rt.jar,它包含了我們最常用的class,如java.lang.*java.util.*等,在虛擬機啟動時會提前加載,這樣用到時就省去了加載耗時,能加快訪問速度。

  2. 運行時加載:大多數(shù)類比如用戶代碼,都是在類第一次被使用時才加載的,也就是常說的惰性加載,這么做的比較直觀的原因大概是節(jié)省內(nèi)存吧。

加載原理

loadClass源碼

先上代碼(JDK1.7源碼):

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;
        }
    }

loadClass是類加載機制中最核心的代碼,這段代碼基本闡述了以下最核心的兩點:

緩存機制

findLoadedClass(name),首先第一步就檢查這個name表示的類是否已被某個類加載器實例加載過,若已加載,則直接返回已加載的c,否則才繼續(xù)下面的委派等邏輯。即JVM層會對已加載的類進行緩存,那具體是怎么緩存的的呢?在JVM實現(xiàn)中,有個類似于HashTable的數(shù)據(jù)結構叫做SystemDictionary,對已加載的類信息進行緩存,緩存的key是類加載器實例+類的全限定名,value則是指向表示類信息的klass數(shù)據(jù)結構。這也是為什么我們常說,JVM中加載類的類加載器實例加上類的全限定名,這兩者一起才能唯一確定一個類。每個類加載器實例就相當于是一個獨立的類命名空間,對于兩個不同類加載器實例加載的類,即便名稱相同,也是兩個完全不同的類。

雙親委派

對于新加載的類,緩存沒命中后走雙親委派邏輯——當parent存在時,會先委派給parent進行loadClass,然后parent.loadClass內(nèi)部又會進行同樣的向上委派,直至parentnull,委派給根加載器。也就是說委派請求會一直向上傳遞,直到頂層的引導類加載器,然后再統(tǒng)一用ClassNotFoundException異常的方式逐層向下回傳,直到某一層classLoader在其搜索范圍內(nèi)找到并加載該類;當parent不存在時,即沒有父類加載器,此時直接委派給頂層加載器——BootstrapClassLoader。

從這里可以看到雙親委派結構中,類加載器之間的父子層級關系并不是通過繼承來實現(xiàn),而是通過組合的方式即子類加載器持有parent代理以指向父類加載器來實現(xiàn)的。

要點
  1. 由于委派是單向的,處于子類加載器層級的類,可以訪問父類加載器層級的類,反過來不行

  2. 各執(zhí)其責,各層級的類加載器只負責加載本層級下的類。實現(xiàn)方式:各層級類加載器有自己的加載路徑,路徑隔離,互不可見。URLClassLoaderucp屬性了解下~

  3. ClassNotFoundException——父子類加載器之間的協(xié)議。只有當父類加載器拋出此異常時,加載請求才會向下層傳遞,其他異常不認!

  4. 上下層的這種優(yōu)先級進一步保證了Java程序的穩(wěn)定性,對于JDK庫中核心的類不會因為用戶誤定義的同名類而導致被覆蓋。

類加載器

還是給個定義吧:

通過一個類的全限定名來獲取描述此類的二進制字節(jié)流的代碼模塊

經(jīng)典的三層加載器結構:

Java類加載機制原理是什么

1、啟動類加載器(或稱為引導類加載器):只負責加載<JAVA_HOME>/lib目錄中的,或是啟動參數(shù)-Xbootclasspath所指定路徑中的特定名稱類庫。該加載器由C++實現(xiàn),對Java程序不可見,對于自定義加載器,若是未指定parent,則會委派該加載器進行加載。

2、擴展類加載器:負責加載<JAVA_HOME>/lib/ext目錄中的,或是java.ext.dirs系統(tǒng)變量所指定的路徑下所有類庫。該加載器由sum.misc.Launcher$ExtClassLoader實現(xiàn),可直接使用。

3、應用程序類加載器(或稱為系統(tǒng)類加載器):負責加載用戶類路徑ClassPath中所有類庫。該加載器由sum.misc.Launcher$AppClassLoader實現(xiàn),可由ClassLoader.getSystemClassLoader()方法獲得。

要點

ExtClassLoaderAppClassLoader都是繼承自URLClassLoader,各自負責的加載路徑都是保存在ucp屬性中,這個看源碼就能得知。

三次“破壞”

雙親委派并不是一個強制性約束模型,畢竟它自身也有局限性。無論是歷史代碼層面、SPI設計問題、還是新的熱部署需求,都不可避免地會違背該原則,累計有三次“破壞”。

可覆蓋的loadClass方法

通過ClassLoader的源碼可知,雙親委派的實現(xiàn)細節(jié)都在loadClass方法中,而該方法是一個protected的,意味著子類可以覆蓋該方法,從而可繞過雙親委派邏輯。雙親委派模型是在JDK1.2之后才被引入,在此之前的JDK1.0,已有部分用戶通過繼承ClassLoader重寫了loadClass邏輯,這使得后面引入的雙親委派邏輯在這些用戶程序中不起作用。

為了向前兼容,ClassLoader新增了findClass方法,提倡用戶將自己的類加載邏輯放入findClass中,而不要再去覆蓋loadClass方法。

自身缺陷,無法支持SPI

雙親委派的層次優(yōu)先級就決定了用戶代碼和JDK基礎類之間的不對等性,即只能用戶代碼調(diào)用基礎類,反之不行。對于SPI之類的設計,比如已經(jīng)成為Java標準服務的JNDI,其接口代碼是在基礎類中,而具體的實現(xiàn)代碼則是在用戶Classpath下,在雙親委派的限制下,JNDI無法調(diào)用實現(xiàn)層代碼。

開個后門——引入線程上下文類加載器(Thread Context ClassLoader),該加載器可通過java.lang.Thread.setContextClassLoader()進行設置,若創(chuàng)建線程時未設置,則從父線程繼承;若應用程序的全局范圍都未設置過,則默認設置為應用程序類加載器,這個可在Launcher的源碼中找到答案。

有了這個,JNDI服務就可使用該加載器去加載所需的SPI代碼。其他類似的SPI設計也是這種方式,如JDBC、JCE、JAXB、JBI等。

程序動態(tài)性的需求,即熱部署

模塊化熱部署,在生產(chǎn)環(huán)境中顯得尤為有吸引力,就像我們的計算機外設一樣,不用重啟,可隨時更換鼠標、U盤等。 OSGi已經(jīng)成為業(yè)界事實上的Java模塊化標準,此時類加載器不再是雙親委派中的樹狀層次,而是復雜的網(wǎng)狀結構。

類加載器實例分析

Tomcat——雙親委派的最佳實踐者

通常Web服務器需要解決幾個基本問題:

  1. 同一個服務器上,部署兩個及以上的Web應用程序,各自使用的Java類庫可以相互隔離。

  2. 多個Web應用程序,共享所使用的部分Java類庫。比如都用到了同樣版本的spring,共享一份,無論是本地磁盤,還是Web服務器內(nèi)存(主要是方法區(qū)),都是不錯的節(jié)省。

  3. 保證服務器自身安全不受部署的Web應用程序影響,這跟前面談到的雙親委派保證Java程序穩(wěn)定性是一個道理。

  4. 支持JSP的話,需要支持HotSwap功能。

為了應對以上基本問題,主流的Java Web服務器都會提供多個Classpath存放類庫。對于Tomcat,其目錄結構劃分為以下4組:

  1. /common目錄,存放的類庫被Tomcat和所有的Web應用程序共享。

  2. /server目錄,僅被Tomcat使用,其他Web應用程序不可見。

  3. /shared目錄,可被所有Web應用程序共享,但對Tomcat不可見。

  4. /WebApp/WEB-INF目錄,僅被所屬的Web應用程序使用,對Tomcat和其他Web應用程序不可見。

跟以上目錄對應的,是Tomcat經(jīng)典的雙親委派類加載器架構:

Java類加載機制原理是什么

上圖中,CommonClassLoader、CatalinaClassLoader、SharedClassLoader和WebAppClassLoader分別負責/common/*/server/*、/shared/*/WebApp/WEB-INF/*目錄下的Java類庫加載,其中WebApp類加載器和Jsp類加載器通常會存在多個實例,每一個Web應用程序對應一個WebAppClassLoader,每一個JSP文件對應一個Jsp類加載器。

OSGi——敢于突破

OSGi(Open Service Gateway Initiative)是OSGi聯(lián)盟制定的一個基于Java語言的動態(tài)模塊化規(guī)范,其最著名的應用案例就是Eclipse IDE,它是Eclipse強大插件體系的基礎。

OSGi中的每個模塊稱為Bundle,一個Bundle可以聲明它所依賴的Java Package(通過Import-Package描述),也可以聲明它允許導出發(fā)布的Java Package(通過Export-Package描述)。Bundle之間的依賴關系為平級依賴,Bundle類加載器之間只有規(guī)則,沒有固定的委派關系。假設存在BundleA、BundleB和BundleC,

BundleA:聲明發(fā)布了packageA,依賴了java.*的包 BundleB:聲明依賴了packageA和packageC,同時也依賴了java.*的包 BundleC:聲明發(fā)布了packageC,依賴了packageA

一個簡單的OSGi類加載器架構示例如下:

Java類加載機制原理是什么

上圖的這種網(wǎng)狀架構帶來了更好的靈活性,但同時也可能產(chǎn)生許多新的隱患。比如Bundle之間的循環(huán)依賴,在高并發(fā)場景下導致加載死鎖。

總結

本文以一個關于類加載的編程題為切入點,闡述了類加載階段的具體細節(jié),包括加載方式、加載時機、加載原理,以及雙親委派的優(yōu)劣點。并以具體的類加載器實例Tomcat和OSGi為例,簡單分析了類加載器在實踐過程中的多種選擇。

“Java類加載機制原理是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關的知識可以關注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實用文章!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI