從覆蓋 JDK 的類開始掌握類的加載機制

JVM 的類加載機制和 Java 的類加載機制相似,但 JVM 的類加載過程稍有些複雜。
JVM 經過加載 .class 文件,可以將其中的字節碼解析成操做系統機器碼。那這些文件是怎麼加載進來的呢?又有哪些約定?接下來咱們就詳細介紹 JVM 的類加載機制,同時介紹三個實際的應用場景。java

類加載過程

現實中並非說,我把一個文件修改爲 .class 後綴,就可以被 JVM 識別。類的加載過程很是複雜,主要有這幾個過程:加載、驗證、準備、解析、初始化。
Cgq2xl4cQNeAO_j6AABZKdVbw1w802.png
如圖所示。大多數狀況下,類會按照圖中給出的順序進行加載。下面咱們就來分別介紹下這個過程。mysql

加載

加載的主要做用是將外部的 .class 文件,加載到 Java 的方法區內。加載階段主要是找到並加載類的二進制數據,好比從 jar 包裏或者 war 包裏找到它們。程序員

驗證

確定不能任何 .class 文件都能加載,那樣太不安全了,容易受到惡意代碼的攻擊。驗證階段在虛擬機整個類加載過程當中佔了很大一部分,不符合規範的將拋出 java.lang.VerifyError 錯誤。像一些低版本的 JVM,是沒法加載一些高版本的類庫的,就是在這個階段完成的。sql

準備

從這部分開始,將爲一些類變量分配內存,並將其初始化爲默認值。此時,實例對象尚未分配內存,因此這些動做是在方法區上進行的。數據庫

下面兩段代碼,code-snippet 1 將會輸出 0,而 code-snippet 2 將沒法經過編譯。
code-snippet 1:
     public class A {
         static int a ;
         public static void main(String[] args) {
             System.out.println(a);
         }
     }
 code-snippet 2:
 public class A {
     public static void main(String[] args) {
         int a ;
         System.out.println(a);
     }
 }
爲何會有這種區別呢?編程

這是由於局部變量不像類變量那樣存在準備階段。類變量有兩次賦初始值的過程,一次在準備階段,賦予初始值(也能夠是指定值);另一次在初始化階段,賦予程序員定義的值。
所以,即便程序員沒有爲類變量賦值也沒有關係,它仍然有一個默認的初始值。但局部變量就不同了,若是沒有給它賦初始值,是不能使用的。tomcat

解析

解析在類加載中是很是很是重要的一環,是將符號引用替換爲直接引用的過程。這句話很是的拗口,其實理解起來也很是的簡單。安全

符號引用是一種定義,能夠是任何字面上的含義,而直接引用就是直接指向目標的指針、相對偏移量。架構

直接引用的對象都存在於內存中,你能夠把通信錄裏的女朋友手機號碼,類比爲符號引用,把面對面和你吃飯的人,類比爲直接引用。app

解析階段負責把整個類激活,串成一個能夠找到彼此的網,過程不可謂不重要。那這個階段都作了哪些工做呢?大致能夠分爲:

  • 類或接口的解析
  • 類方法解析
  • 接口方法解析
  • 字段解析

咱們來看幾個常常發生的異常,就與這個階段有關。

  • java.lang.NoSuchFieldError 根據繼承關係從下往上,找不到相關字段時的報錯。
  • java.lang.IllegalAccessError 字段或者方法,訪問權限不具有時的錯誤。
  • java.lang.NoSuchMethodError 找不到相關方法時的錯誤。

解析過程保證了相互引用的完整性,把繼承與組合推動到運行時。

初始化

若是前面的流程一切順利的話,接下來該初始化成員變量了,到了這一步,才真正開始執行一些字節碼。

接下來是一道試題,你能夠猜測一下,下面的代碼,會輸出什麼?
public class A {
     static int a = 0 ;
     static {
         a = 1;
         b = 1;
     }
     static int b = 0;
 
     public static void main(String[] args) {
         System.out.println(a);
         System.out.println(b);
     }
 }
結果是 1 0。a 和 b 惟一的區別就是它們的 static 代碼塊的位置。

這就引出一個規則:static 語句塊,只能訪問到定義在 static 語句塊以前的變量。因此下面的代碼是沒法經過編譯的。
static {
         b = b + 1;
 }
 static int b = 0;
咱們再來看第二個規則:JVM 會保證在子類的初始化方法執行以前,父類的初始化方法已經執行完畢。

因此,JVM 第一個被執行的類初始化方法必定是 java.lang.Object。另外,也意味着父類中定義的 static 語句塊要優先於子類的。

<cinit>與<init>

說到這裏,不得再也不說一個試題:<cinit> 方法和 <init> 方法有什麼區別?

主要是爲了讓你弄明白類的初始化和對象的初始化之間的差異。
public class A {
     static {
         System.out.println("1");
     }
     public A(){
         System.out.println("2");
         }
     }
 
     public class B extends A {
         static{
         System.out.println("a");
     }
     public B(){
         System.out.println("b");
     }
 
     public static void main(String[] args){
         A ab = new B();
         ab = new B();
     }
 }
先公佈下答案:
1
a
2
b
2
b
你能夠看下這張圖。其中 static 字段和 static 代碼塊,是屬於類的,在類的加載的初始化階段就已經被執行。類信息會被存放在方法區,在同一個類加載器下,這些信息有一份就夠了,因此上面的 static 代碼塊只會執行一次,它對應的是 <cinit> 方法。

而對象初始化就不同了。一般,咱們在 new 一個新對象的時候,都會調用它的構造方法,就是 <init>,用來初始化對象的屬性。每次新建對象的時候,都會執行。
CgpOIF4cQNeAYYhRAADbeRet_7k581.png
因此,上面代碼的 static 代碼塊只會執行一次,對象的構造方法執行兩次。再加上繼承關係的前後原則,不難分析出正確結果。

類加載器

整個類加載過程任務很是繁重,雖然這活兒很累,但總得有人幹。類加載器作的就是上面 5 個步驟的事。

若是你在項目代碼裏,寫一個 java.lang 的包,而後改寫 String 類的一些行爲,編譯後,發現並不能生效。JRE 的類固然不能輕易被覆蓋,不然會被別有用心的人利用,這就太危險了。

那類加載器是如何保證這個過程的安全性呢?其實,它是有着嚴格的等級制度的。

幾個類加載器

首先,咱們介紹幾個不一樣等級的類加載器。

Bootstrap ClassLoader

這是加載器中的大 Boss,任何類的加載行爲,都要經它過問。它的做用是加載核心類庫,也就是 rt.jar、resources.jar、charsets.jar 等。固然這些 jar 包的路徑是能夠指定的,-Xbootclasspath 參數能夠完成指定操做。

這個加載器是 C++ 編寫的,隨着 JVM 啓動。

Extention ClassLoader

擴展類加載器,主要用於加載 lib/ext 目錄下的 jar 包和 .class 文件。一樣的,經過系統變量 java.ext.dirs 能夠指定這個目錄。

這個加載器是個 Java 類,繼承自 URLClassLoader。

App ClassLoader

這是咱們寫的 Java 類的默認加載器,有時候也叫做 System ClassLoader。通常用來加載 classpath 下的其餘全部 jar 包和 .class 文件,咱們寫的代碼,會首先嚐試使用這個類加載器進行加載。

Custom ClassLoader

自定義加載器,支持一些個性化的擴展功能。

雙親委派機制

雙親委派機制的意思是除了頂層的啓動類加載器之外,其他的類加載器,在加載以前,都會委派給它的父加載器進行加載。這樣一層層向上傳遞,直到祖先們都沒法勝任,它纔會真正的加載。

打個比方。有一個家族,都是一些聽話的孩子。孫子想要買一塊棒棒糖,最終都要通過爺爺過問,若是力所能及,爺爺就直接幫孫子買了。

但你有沒有想過,「類加載的雙親委派機制,雙親在哪裏?明明都是單親?」
咱們仍是用一張圖來說解。能夠看到,除了啓動類加載器,每個加載器都有一個parent,並無所謂的雙親。可是因爲翻譯的問題,這個叫法已經很是廣泛了,必定要注意背後的差異。
Cgq2xl4cQNeAG0ECAAA_CbVCY1M014.png
咱們能夠翻閱 JDK 代碼的 ClassLoader#loadClass 方法,來看一下具體的加載過程。和咱們描述的同樣,它首先使用 parent 嘗試進行類加載,parent 失敗後才輪到本身。同時,咱們也注意到,這個方法是能夠被覆蓋的,也就是雙親委派機制並不必定生效。
CgpOIF4cQNeACEs8AACe317zgN8195.jpg
這個模型的好處在於 Java 類有了一種優先級的層次劃分關係。好比 Object 類,這個毫無疑問應該交給最上層的加載器進行加載,即便是你覆蓋了它,最終也是由系統默認的加載器進行加載的。

若是沒有雙親委派模型,就會出現不少個不一樣的 Object 類,應用程序會一片混亂。

一些自定義加載器

下面咱們就來聊一聊能夠打破雙親委派機制的一些案例。爲了支持一些自定義加載類多功能的需求,Java 設計者其實已經做出了一些妥協。

案例一:tomcat

tomcat 經過 war 包進行應用的發佈,它實際上是違反了雙親委派機制原則的。簡單看一下 tomcat 類加載器的層次結構。
Cgq2xl4cQNeAZ4FuAABzsqSozok762.png
對於一些須要加載的非基礎類,會由一個叫做 WebAppClassLoader 的類加載器優先加載。等它加載不到的時候,再交給上層的 ClassLoader 進行加載。這個加載器用來隔毫不同應用的 .class 文件,好比你的兩個應用,可能會依賴同一個第三方的不一樣版本,它們是相互沒有影響的。

如何在同一個 JVM 裏,運行着不兼容的兩個版本,固然是須要自定義加載器才能完成的事。

那麼 tomcat 是怎麼打破雙親委派機制的呢?能夠看圖中的 WebAppClassLoader,它加載本身目錄下的 .class 文件,並不會傳遞給父類的加載器。可是,它卻可使用 SharedClassLoader 所加載的類,實現了共享和分離的功能。
可是你本身寫一個 ArrayList,放在應用目錄裏,tomcat 依然不會加載。它只是自定義的加載器順序不一樣,但對於頂層來講,仍是同樣的。

案例二:SPI

Java 中有一個 SPI 機制,全稱是 Service Provider Interface,是 Java 提供的一套用來被第三方實現或者擴展的 API,它能夠用來啓用框架擴展和替換組件。

這個說法可能比較晦澀,可是拿咱們經常使用的數據庫驅動加載來講,就比較好理解了。在使用 JDBC 寫程序以前,一般會調用下面這行代碼,用於加載所須要的驅動類。
Class.forName("com.mysql.jdbc.Driver")
這只是一種初始化模式,經過 static 代碼塊顯式地聲明瞭驅動對象,而後把這些信息,保存到底層的一個 List 中。
可是你會發現,即便刪除了 Class.forName 這一行代碼,也能加載到正確的驅動類,什麼都不須要作,很是的神奇,它是怎麼作到的呢?
咱們翻開 MySQL 的驅動代碼,發現了一個奇怪的文件。之因此可以發生這樣神奇的事情,就是在這裏實現的。
路徑:
mysql-connector-java-8.0.15.jar!/META-INF/services/java.sql.Driver
裏面的內容是:
com.mysql.cj.jdbc.Driver
經過在 META-INF/services 目錄下,建立一個以接口全限定名爲命名的文件(內容爲實現類的全限定名),便可自動加載這一種實現,這就是 SPI。

SPI 其實是「基於接口的編程+策略模式+配置文件」組合實現的動態加載機制,主要使用 java.util.ServiceLoader 類進行動態裝載。
CgpOIF4cQNeARP3IAAA2VH9MXoY723.jpg
這種方式,一樣打破了雙親委派的機制。

DriverManager 類和 ServiceLoader 類都是屬於 rt.jar 的。它們的類加載器是 Bootstrap ClassLoader,也就是最上層的那個。而具體的數據庫驅動,卻屬於業務代碼,這個啓動類加載器是沒法加載的。這就比較尷尬了,雖然凡事都要祖先過問,但祖先沒有能力去作這件事情,怎麼辦?

咱們能夠一步步跟蹤代碼,來看一下這個過程。
//part1:DriverManager::loadInitialDrivers
  //jdk1.8 以後,變成了lazy的ensureDriversInitialized
  ...
  ServiceLoader <Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  Iterator<Driver> driversIterator = loadedDrivers.iterator();
  ...

  //part2:ServiceLoader::load
  public static <T> ServiceLoader<T> load(Class<T> service) {
      ClassLoader cl = Thread.currentThread().getContextClassLoader();
      return ServiceLoader.load(service, cl);
  }
經過代碼你能夠發現 Java 玩了個魔術,它把當前的類加載器,設置成了線程的上下文類加載器。那麼,對於一個剛剛啓動的應用程序來講,它當前的加載器是誰呢?也就是說,啓動 main 方法的那個加載器,究竟是哪個?

因此咱們繼續跟蹤代碼。找到 Launcher 類,就是 jre 中用於啓動入口函數 main 的類。咱們在 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);
 ...
 }
到此爲止,事情就比較明朗了,當前線程上下文的類加載器,是應用程序類加載器。使用它來加載第三方驅動,是沒有什麼問題的。

案例三:OSGi

OSGi 曾經很是流行,Eclipse 就使用 OSGi 做爲插件系統的基礎。OSGi 是服務平臺的規範,旨在用於須要長運行時間、動態更新和對運行環境破壞最小的系統。

OSGi 規範定義了不少關於包生命週期,以及基礎架構和綁定包的交互方式。這些規則,經過使用特殊 Java 類加載器來強制執行,比較霸道。

好比,在通常 Java 應用程序中,classpath 中的全部類都對全部其餘類可見,這是毋庸置疑的。可是,OSGi 類加載器基於 OSGi 規範和每一個綁定包的 manifest.mf 文件中指定的選項,來限制這些類的交互,這就讓編程風格變得很是的怪異。但咱們不難想象,這種與直覺相違背的加載方式,確定是由專用的類加載器來實現的。

隨着 jigsaw 的發展(旨在爲 Java SE 平臺設計、實現一個標準的模塊系統),我我的認爲,如今的 OSGi,意義已經不是很大了。OSGi 是一個龐大的話題,你只須要知道,有這麼一個複雜的東西,實現了模塊化,每一個模塊能夠獨立安裝、啓動、中止、卸載,就能夠了。

如何替換 JDK 的類

如何替換 JDK 中的類?好比,咱們如今就拿 HashMap爲例。

當 Java 的原生 API 不能知足需求時,好比咱們要修改 HashMap 類,就必需要使用到 Java 的 endorsed 技術。咱們須要將本身的 HashMap 類,打包成一個 jar 包,而後放到 -Djava.endorsed.dirs 指定的目錄中。注意類名和包名,應該和 JDK 自帶的是同樣的。可是,java.lang 包下面的類除外,由於這些都是特殊保護的。

由於咱們上面提到的雙親委派機制,是沒法直接在應用中替換 JDK 的原生類的。可是,有時候又不得不進行一下加強、替換,好比你想要調試一段代碼,或者比 Java 團隊早發現了一個 Bug。因此,Java 提供了 endorsed 技術,用於替換這些類。這個目錄下的 jar 包,會比 rt.jar 中的文件,優先級更高,能夠被最早加載到。

小結

一個 Java 類的加載,通過了加載、驗證、準備、解析、初始化幾個過程,每個過程都劃清了各自負責的事情。

Java 自帶的三個類加載器。main 方法的線程上下文加載器,實際上是 Application ClassLoader。

通常狀況下,類加載是遵循雙親委派機制的。咱們也認識到,這個雙親,頗有問題。經過 3 個案例的學習和介紹,能夠看到有不少打破這個規則的狀況。類加載器經過開放的 API,讓加載過程更加靈活。

不管是遠程存儲字節碼,仍是將字節碼進行加密,這都是業務需求。要作這些,咱們實現一個新的類加載器就能夠了。

相關文章
相關標籤/搜索