JVM:Java 類的加載機制

虛擬機把描述類的數據從 Class 文件加載到內存,並對數據進行校驗,轉換,解析和初始化,最終造成能夠被虛擬機直接使用的 Java 類型,這就是虛擬機的類加載機制。java

類的生命週期

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括了:加載、驗證、準備、解析、初始化、使用、卸載七個階段。其中驗證、準備和解析三個部分統稱爲鏈接。程序員

img

  • 加載:加載是類加載的第一個階段,這個階段,首先要根據類的全限定名來獲取定義此類的二進制字節六,講字節六轉化爲方法區運行時數據結構。在 Java 堆生成一個表明這個類的 java.lang.class 對象,做爲方法區的訪問入口。web

  • 驗證:這一步的的目的是確保 class 文件的字節六包含的信息符合虛擬機的要求。sql

  • 準備:準備階段是正式爲類變量分配內存並設置類變量初始值的階段,這些內存都會在方法區中進行分配。僅僅是類變量,不包括實例變量。數據庫

    public static int value = 123;

    變量在準備階段事後的初始值爲0而不是123,123的賦值要在變量初始化之後纔會完成。tomcat

  • 解析:虛擬機將常量池內的符號引用替換爲直接引用的過程。安全

  • 初始化:初始化是類加載的最後一步,這一步會根據程序員給定的值去初始化一些資源。數據結構

加載是咱們使用一個類的第一步,加載是如何完成的那?app

類加載器

虛擬機設計團隊把類加載階段中的經過一個類的全限定名來獲取描述此類的二進制字節流這個動做放到 Java 虛擬機外部去實現,以便讓程序本身去決定如何獲取所須要的類,這個動做的代碼模塊稱爲類加載器webapp

對於一個類,都須要由加載它的類加載器和這個類自己一同確立其在 Java 虛擬機中的惟一性,比較兩個類是否相等須要在這兩個類是由同一個類加載器加載的前提下才有意義。

  • 啓動類加載器(Bootstrap ClassLoader):這個類負責將 \lib 目錄中的類庫加載到內存中,啓動類加載器沒法被 Java 程序直接引用。
  • 擴展類加載器(Extension ClassLoader):負責加載 \lib\ext 目錄中的類。開發者能夠直接使用擴展類加載器。
  • 應用程序類加載器(Application ClassLoader):這個類加載器是 ClassLoader 中 getSystemClassLoader() 方法的返回值,因此通常稱爲系統類加載器。若是沒有自定義過加載器,通常狀況下這個就是默認的類加載器。
  • 自定義類加載器(User ClassLoader):經過自定義類加載器能夠實現一些動態加載的功能,好比 SPI。

雙親委派模型

JVM 在加載類時默認採用的是雙親委派模型機制。通俗地講,某個特定的類加載器在接到加載類的請求時,首先講加載任務委託給父類加載器,所以全部加載請求最總都應該傳送到頂層的啓動類加載器中。若是父類沒法完成加載請求,子類纔會嘗試本身加載。

因此,越基礎的類會由越上層的加載器加載。

若是不使用雙親委派模型,用戶本身寫一個 Object 類放入 ClassPath,那麼系統中將會出現多個不一樣的 Object 類,Java 類型體系中最基礎的行爲也就無從保證。如今你能夠嘗試本身寫一個名爲 Object 的類,能夠被編譯,但永遠沒法運行。由於最後加載時都會先委派給父類去加載,在 rt.jar 搜尋自身目錄時就會找到系統定義的 Object 類,因此你定義的 Object 類永遠沒法被加載和運行。

img

雙親委派模型的好處是保證了核心類庫不配覆蓋和篡改。

打破雙親委派模型

雙親委派模型並非一個強制性的約束模型,而是 Java 設計者推薦給開發者的類加載器實現方式。Java 世界中大部分的類加載器都遵循這個模型。

雙親委派模型第一次「被破壞」是由這個模型自身的缺陷致使的,雙親委派很好地解決了各個類加載器的基礎類的統一問題(越基礎的類由越上層的加載器進行加載),基礎類之因此稱爲「基礎」,是由於他們老是做爲被用戶代碼調用的 API。那若是基礎類又要調用回用戶的代碼,怎麼辦?

好比 JNDI 服務,JNDI 如今是 Java 的標準服務,他的代碼由啓動類加載器去加載(rt.jar),但 JNDI 須要由獨立廠商實現並部署在應用程序的 Class Path 下的 JNDI 接口提供者的代碼,啓動類加載器不可能認識這些代碼,由於啓動類加載器的搜索範圍找不到用戶應用程序類。

爲了解決這個問題,Java 團隊設計了一個不太優雅的設計:線程上下文加載器這個類加載器能夠經過java.lang.Thread類的setContextClassLoader() 方法進行設置,若是建立線程時還未設置,它將會從父線程中繼承一個,若是在應用程序的全局範圍內都沒有設置過的話,那這個類加載器默認就是應用程序類加載器(Application ClassLoader)。

有了這個線程上下文加載器,JNDI 服務使用線程上下文加載器去加載所須要的 SPI 代碼。也就是父類加載器請求子類加載器完成類加載的動做,這就打破了雙親委派模型。典型的例子有 JNDI 和 JDBC 等。

Tomcat 類加載機制

Tomcat 的類加載機制是違反了雙親委派原則的,對於一些爲加載的非基礎類(Object,String)等,各個 web 應用本身的類加載器(WebAppClassLoader)會優先加載,加載不到時再交給 commonClassLoader 走雙親委派模型。總體結構以下:

Tomcat 類加載器結構

這其中 JDK 提供的類加載器分別是:

  • Bootstrap-啓動類加載器,JVM 的一部分,加載 /lib/ 目錄下特定的文件
  • Extension-擴展類加載器,加載 /lib/ext/ 目錄下的類庫。
  • Application-應用程序類加載器,也叫系統類加載器,加載 CLASSPATH 指定的類庫。

Tomcat 自定義類加載器分別是:

  • Common - 父加載器是 AppClassLoader,默認加載 ${catalina.home}/lib/ 目錄下的類庫
  • Catalina - 父加載器是 Common 類加載器,加載 catalina.properties 配置文件中 server.loader 配置的資源,通常是 Tomcat 內部使用的資源
  • Shared - 父加載器是 Common 類加載器,加載 catalina.properties 配置文件中 shared.loader 配置的資源,通常是全部 Web 應用共享的資源
  • WebappX - 父加載器是 Shared 加載器,加載 /WEB-INF/classes 的 class 和 /WEB-INF/lib/ 中的 jar 包
  • JasperLoader - 父加載器是 Webapp 加載器,加載 work 目錄應用編譯 JSP 生成的 class 文件

JDBC 爲何要破壞雙親委派模型?

在 JDBC 4.0 以後咱們須要使用 Class.forName 來加載驅動程序了,咱們只須要把驅動的 jar 包放到工程的類加載路徑裏,那麼驅動就會被自動加載。

這個自動加載採用的技術叫 SPI,能夠看一下jar包裏面的META-INF/services目錄,裏面有一個java.sql.Driver的文件,文件裏面包含了驅動的全路徑名。咱們只須要下面這一句話就能夠建立數據庫鏈接:

Connection con =    
             DriverManager.getConnection(url , username , password ) ;

由於類加載器受到加載範圍的限制,在某些狀況下父類加載器沒法加載到所須要的文件,這時候就須要委託子類加載器去加載 class 文件

JDBC 的 Driver 接口定義在 JDK 中,其實現由各個數據庫服務商來提供,好比 MySQL 的驅動包。DriverManager 類中要加載各個實現了Driver接口的類,而後進行管理,可是DriverManager位於 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap類加載器加載,而其Driver接口的實現類是位於服務商提供的 Jar 包,根據類加載機制,當被裝載的類引用了另一個類的時候,虛擬機就會使用裝載第一個類的類裝載器裝載被引用的類。也就是說 Bootstrap 類加載器還要去加載 jar 包中的 Driver 接口的實現類。Bootstrap 只負責 /lib/rt.jar 裏面全部的 class,因此須要子類加載器去加載 Driver,這就破壞了雙親委派模型。

查看 DriverManager 類的源碼,看到使用 DriverManager 的時候會觸發其靜態代碼塊,調用 loadInitialDrivers() 方法,並調用 ServiceLoader.load(Driver.class) 加載全部在META-INF/services/java.sql.Driver 文件裏邊的類到JVM內存,完成驅動的自動加載。

static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

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

    }
public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

這個子類加載器是經過 Thread.currentThread().getContextClassLoader() 獲得的上下文加載器。

public Launcher() {
    ...
    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);
    ...
}

能夠看到,在 sun.misc.Launcher 初始化的時候,會獲取AppClassLoader,而後將其設置爲上下文類加載器,因此線程上下文類加載器默認狀況下就是系統加載器

Tomcat 爲何要破壞雙親委派模型

每一個 Tomcat 的 webappClassLoader 加載本身目錄的 class 文件,不會傳遞給父類加載器

事實上,Tomcat 之因此造出了一堆本身的 classLoader,大體是出於三個目的:

  1. 對於各個 weapp 中的 class 和 lib,須要相互隔離,不能出現一個應用中加載的類影響另外一個應用的狀況。
  2. 與 JVM 同樣出於安全考慮,使用單獨的 classLoader 去裝載 Tomcat 自身的類庫,防止惡意或者無心的破壞。
  3. 熱部署。
相關文章
相關標籤/搜索