1. 程式人生 > 程式設計 >面試高頻:深入理解Java虛擬機器器之—JVM類載入過程和類載入器

面試高頻:深入理解Java虛擬機器器之—JVM類載入過程和類載入器

深入理解Java虛擬機器器之—JVM類載入過程和類載入器

不僅是為了面試,還為了從根本上學習和理解Java程式碼的執行過程,提高自己對Java的理解

Java虛擬機器器生命週期:

  1. 程式正常結束
  2. 程式異常終止
  3. 作業系統錯誤
  4. System.exit()

類載入

新增idea屬性列印載入的類 -XX:+TraceClassLoading

在Java程式碼中,類的載入、連線和初始化都是在執行時完後的,每一個類都通過類載入器加入載入到JVM中(堆中),形成一個虛擬機器器可以直接使用的Java型別

1.載入

把Java位元組碼載入成一段二進位制流,讀取到記憶體,放在執行時資料區的方法區內;建立一個java.lang.Class物件描述該類的資料結構

可以從磁碟、jar、war、網路、自己編寫的class檔案中載入class檔案

2.連線

分為驗證、準備、解析三個階段

(1)驗證

確保類載入的正確性,保證Class檔案的位元組流不會影響虛擬機器器的安全(因為class檔案可以從任何途徑生成),驗證失敗丟擲VerifyError,驗證通過就把記憶體中的二進位制流存放到JVM的執行時資料區的方法區中

  1. 檔案格式驗證

檔案開頭魔數代表JDK版本號等資訊;常量池中是否有不支援的常量

只有驗證通過,二進位制位元組流才會進入記憶體的方法區儲存

  1. 元資料驗證

驗證該類是否有父類,父類是否繼承了不允許繼承的類(final類);是否實現了父類或者介面中要求實現的方法;類中方法欄位是否與父類或者介面匹配(引數型別、返回值型別)

  1. 位元組碼驗證

對類的方法體進行驗證,保證型別轉換是安全的。

通過位元組碼驗證也不一定是安全的,Halting Problem,沒有任何一個程式可以校驗所有程式的合法性(比如while true是無法校驗的)

  1. 符號引用驗證

發生在符號引用轉換為直接引用的時候

確保該符號引用可以找到對應類。

(2)準備

為類的靜態變數分配記憶體(記憶體中方法區),並將其初始化為預設值(不是自己設定的值,例如int a=1;將a賦值為0)

(3)解析

將虛擬機器器常量池中的符號引用(一組符號描述目標引用,也就是JVM中的Reference)轉換為直接引用(指向目標的實際記憶體地址)

3.初始化

  • 被動使用不會導致類的初始化

為靜態變數賦初始值,執行static塊

以下情況將觸發初始化:

  1. 遇到new,getstatic,putstatic,invokestatic指令時,如果沒有初始化將進行初始化
  2. 反射呼叫reflect包中,將初始化呼叫類
  3. 虛擬機器器啟動時需要制定一個執行的主類,main函式類將進行初始化
  4. 初始化一個類時,父類沒有被初始化,則將進行父類初始化
  5. JDK7中MethodHandler

對於靜態欄位,只有直接定義的地方才會被初始化

public class Test8 {
    public static void main(String[] args) {
        System.out.println(Son2.s);
    }
}
class Father2{
    public static int s = 1;
    static{
        System.out.println("hello i am father");
    }
}
class Son2 extends Father2{
    //不會列印這句 沒有對Son2的主動使用
    static {
        System.out.println("hello i am son");
    }
}
複製程式碼

在初始化一個類時,要求其父類已經被初始化

在初始化一個介面時,不要求其父介面被初始化

在初始化一個類時,不要求其實現介面被初始化

介面變數不需要使用public static final修飾 預設是常量

案例:載入靜態變數和常量

public class Test1 {
    public static void main(String[] args) {
        System.out.println(MyChild.s);
    }
}
class MyParent{
    /**
     * 當s申明為static時 會載入父類和子類,但是隻會呼叫父類的static塊
     * 當s加上final時,表示常量,不會載入任何一個類,編譯階段被放入該Test1類的常量池中
     */
    public static final String s = "dx";
    static {
        System.out.println("hello i am my parent");
    }
}
class MyChild extends MyParent{
    static {
        System.out.println("i am my child");
    }
}複製程式碼

案例:介面初始化

/**
 * 介面初始化時,不要求父介面被初始化完成
 * 常量如果編譯時確定,就不會去載入
 * 如果時執行時才可以確定的常量,需要載入
 */
public class Test4 {
    public static void main(String[] args) {
        System.out.println(MyInterfaceSon.b);
    }
}
//一直不載入
interface MyInterface{
    public static final int  a = 5;
}
interface MyInterfaceSon extends MyInterface{
    //會載入,執行時確定
    public static final int  b = new Random().nextInt(10);
    //不會載入,編譯時就已經確定
    //public static final int  b = 10;

}複製程式碼

案例:物件陣列不被載入

public class Test3 {
    public static void main(String[] args) {
        /*
         * 不會載入MyParen4,陣列型別不會導致載入,只會建立陣列引用分配空間
         */
        MyParent3[] myParent = new MyParent3[10];
        //class [Ltop.dzou.jvm.MyParent3;
        //陣列型別標誌 [L 全限定名
        System.out.println(myParent.getClass());
    }
}
class MyParent3{

    static{
        System.out.println("i am my parent3");
    }
}複製程式碼

案例:靜態常量的初始化

public class Test5 {
    public static void main(String[] args) {
        /**
         * 呼叫了getInstance方法 主動進行載入Singleton類
         * 準備階段:初始化count1為0 singleton為null count2為0
         * 初始化完成後,按照順序呼叫,執行了invokespecial執行了建構函式,執行完count1=1 count2=1
         * 呼叫完後執行了自己的putstatic指令 把count2設定為0
         * 最終結果:count1=0 count2=0
         */
        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton.count1);
        System.out.println(singleton.count2);
    }
}
class Singleton{
    public static int count1;
    private static Singleton singleton = new Singleton();
    private Singleton(){
        count1++;count2++;
        System.out.println(count1);
        System.out.println(count2);
    }
    public static int count2 = 0;

    public static Singleton getInstance(){
        return singleton;
    }
}複製程式碼

雙親委託機制

載入一個類時,會由自底向上檢查一個類是否被載入,如果沒有被載入過,會嘗試從頂向下載入,首先會由啟動器載入器rt.jar載入Object,所有類被載入時都要保證Object類已經被載入

包含關係:

子載入器包含一個父親載入器的引用,即使兩個載入器屬於一種型別的載入器(例如:同一種自定義載入器)

利用的是ClassLoader中構造方法可以傳入一個parent也就是指向父類的類載入器的引用,載入時會優先委託給父類

面試題:

是否可以自定義一個java.lang.System類?

答:不行,因為自定義System在載入時會被委託到啟動器類載入器載入,根據全限定名找到真正的System類載入後在執行main函式時會報找不到main方法,原因是自定義的System類不會被載入

public class System {
    public static void main(String[] args) {

    }
}

output:
錯誤: 在類 java.lang.System 中找不到 main 方法,請將 main 方法定義為:
   public static void main(String[] args)
否則 JavaFX 應用程式類必須擴充套件javafx.application.Application複製程式碼

雙親委派模型優點:

  1. 保證核心庫的安全:如果都有自己的載入器載入,那麼會存在很多名稱空間,會存在很多相同的類,但是無法相互相容使用(名稱空間不同),確保核心類被優先載入
  2. JVM相同的類可以存在的,通過名稱空間相互隔離,可以一同存在,在不同名稱空間中可以使用。

類載入器剖析

類載入器

JVM虛擬機器器類載入器:啟動器載入器擴充套件類載入器系統載入器

類載入器就是根據一個全限定名載入class生成二進位制流並轉換為一個java.lang.Class物件例項

  • 真正類的載入過程是由defineClass完成的,根據Java Doc
Converts an array of bytes into an instance of class Class. Before the Class can be used it must be resolved.複製程式碼

它將一個二進位制流轉換為一個java.lang.Class物件返回

名稱空間

  • 每個類載入器都有自己的名稱空間。
  • 同一個名稱空間內的類是相互可見的,名稱空間由該載入器及所有父載入器所載入的類組成。
  • 在同一個名稱空間中,不會出現類的完整名字(包括類的包名)相同的兩個類;在不同的名稱空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類。

擴充套件類載入器載入的class檔案需要打成jar包

更改系統類載入器目錄:修改java.system.class.loader為自定義

命令:java -Djava.system.class.loader /自定義載入器class檔案路徑

方法
作用
loadClass(String name)
載入名稱為 name的類,返回的結果是 java.lang.Class類的例項。
findClass(String name)
查詢名稱為 name的類,返回的結果是 java.lang.Class類的例項。
findLoadedClass(String name)
查詢名稱為 name的已經被載入過的類,返回的結果是 java.lang.Class類的例項。
defineClass(String name,byte[] b,int off,int len) 把位元組陣列 b中的內容轉換成 Java 類,返回的結果是 java.lang.Class類的例項。這個方法被宣告為 final的。
resolveClass(Class> c)
連結指定的 Java 類。

{% qnimg jvm/4.png %}

案例:反射不導致類的初始化

public class Test9 {
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = ClassLoader.getSystemClassLoader();
        //classloader不會導致類的初始化
        Class<?> c = classLoader.loadClass("top.dzou.jvm.class_load.D");
        System.out.println("---------");
        //使用反射載入類會導致類的主動使用,從而初始化該類
        Class.forName("top.dzou.jvm.class_load.D");
        System.out.println(c);;
    }
}
class D{
    static {
        System.out.println("hello i am d");
    }
}複製程式碼

案例:實現一個類載入器

對於自定義的類載入器,我們通過繼承ClassLoader類呼叫子類的loadClass方法載入類,loadClass方法會為我們自動呼叫findClass方法,其中需要實現自定義的載入類以及實現defineClass方法

public class Test10 extends ClassLoader{
    private String fileExt = ".class";
    private String path = null;
    public void setPath(String path) {
        this.path = path;
    }
    public Test10(){
        super();//super方法會使用系統載入器作為預設類載入器
    }
    @Override
    protected Class<?> findClass(String s) throws ClassNotFoundException {
        byte[] data = loadClassData(s);
        //找到class呼叫核心defineClass方法返回一個Class物件
        return defineClass(s,data,data.length);
    }
    //自己實現的載入類方法,把檔案讀取到二進位制流中返回
    public byte[] loadClassData(String fileName){
        InputStream in = null;
        ByteArrayOutputStream baos = null;
        byte[] data = null;
        try {
            fileName = fileName.replace(".","/");
            in = new FileInputStream(new File(path+fileName+this.fileExt));
            baos = new ByteArrayOutputStream();
            int c = 0;
            while((c=in.read())!=-1){
                baos.write(c);
            }
            data = baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                in.close();
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }

    public static void main(String[] args) throws IllegalAccessException,InstantiationException,ClassNotFoundException {
        Test10 loader = new Test10();
        //呼叫ClassLoader的loadClass方法
        loader.setPath("/home/dzou/java/jvm-learning/target/classes/");
        Class<?> c = loader.loadClass("top.dzou.jvm.class_load.Test9");
        System.out.println("class:"+c);
        Object o = c.newInstance();
        System.out.println(o);
        System.out.println(o.getClass().getClassLoader());
    }
}複製程式碼

注意:根據雙親委託機制,會先交給父類去載入,也就是系統類載入器載入,系統類載入器能載入成功的話,就不會使用我們自定義的類載入器,所以我們需要把target中的.class檔案刪除,使用我們自定義的.class檔案路徑才會讓系統類載入器載入失敗,從而使用我們自定義的類載入器

名稱空間使用

兩個不同例項的載入器載入不同path下的class

public class Test13 {
    public static void main(String[] args) throws Exception {
        Test10 loader1 = new Test10();
        Test10 loader2 = new Test10();
        loader1.setPath("/home/dzou/Downloads/j/classes/");
        loader2.setPath("/home/dzou/Downloads/a/");
        Class<?> clazz2 = loader2.loadClass("top.dzou.jvm.class_load.Test1");
        Class<?> clazz1 = loader1.loadClass("top.dzou.jvm.class_load.Test1");
        Object o1 = clazz1.newInstance();
        Object o2 = clazz2.newInstance();
        System.out.println(o1.getClass().getClassLoader());
        System.out.println(o2.getClass().getClassLoader());
        System.out.println(o1==o2);
    }
}

輸出:
top.dzou.jvm.class_load.Test10@6f94fa3e
top.dzou.jvm.class_load.Test10@1d44bcfa
false複製程式碼

繼承關係

Launcher系統和擴充套件類載入類->ExtClassLoader/AppClassLoader內部類->URLClassLoader支援通過路徑和jar包載入->SecureClassLoader支援提供保護permissions許可權(具體沒有了解)->ClassLoader

任意兩個載入器都可以通過構造方法建立父子關係,即使是同一個類的類載入器

上下文類載入器

ContextClassLoader就是為了破壞Java雙親委派模型

我們瞭解了類載入器,現在看一下一個核心的載入器,就是上下文類載入器ContextClassLoader

我們可以通過Thread.currentThread().getContextClassLoader()獲取當前上下文類載入器

通過Thread.currentThread().setContextClassLoader(ClassLoader cl);來設定上下文類載入器

依賴規則:我們知道每一個類都會使用自己的類載入器載入該類中依賴的類,比如A類中引用了B類,那麼載入A類的時候就會使用載入A的載入器載入B,而且每一個我們編寫的類都是由系統類載入器(AppClassLoader)載入的,那

  • 為何出現上下文類載入器?

知道SPI的同學可能就知道JDBC、JAXP,不瞭解的下面一節會講到,他們都是基於SPI實現的,基本上說就是JDK提供介面,服務商提供不同的實現(jar包),當我們使用這些SPI介面時,我們都要匯入相應的jar包到classpath下的指定目錄可能為lib,mysql-connectorJ等,但是我們的SPI介面是在rt.jar中的,是由啟動器類為我們載入的,那麼如果根據依賴規則和雙親委派模型,JVM會使用載入該介面類的啟動器載入器來載入我們的介面實現類,但是我們的SPI的不同實現類卻在classpath下,這裡是啟動器類載入器載入不到的,classpath只能由系統類載入器或者自定義載入器載入,那麼這樣就會導致無法載入SPI介面實現類,所以雙親委派模型就不能在這起到合適的作用,我們就只能想辦法去讓系統載入器來支援載入SPI實現類,於是出現了上下文類載入器

可能有人會說直接把各個廠商的實現放入對應的介面類所在包裡不就好了,乍一看這麼做是可以解決問題,但是你要知道的是無論在設計模式還是JDK中都是面向擴充套件,對修改關閉的,這樣做不僅違背了設計模式還會讓JDK包變的務必龐大

  • 上下文類載入器的作用?

它改變了父載入器的載入方式,也就是破壞了雙親委託模型,它讓父載入器可以使用當前執行緒的Thread.currentThread().getContextClassLoader()`類載入器獲取到載入classpath下類的載入器,使用該載入器去載入類,這就改變了父載入器不能使用子載入器載入的類的情況

根據雙親委派模型傳遞順序,父類載入器載入不了才會交給子類載入器,所以它自然看不到並無法載入子類載入器載入的類,智慧的JDK開發者發現了這一點,想到了一個執行緒中的類載入器,就可以通過執行緒的上下文類載入器來讓父載入器可以訪問子載入器所載入的類,就相當於把系統類載入器放在當前執行緒的上下文類載入器中,當父載入器需要獲取子類載入器載入的類時,就可以通過這種方式獲取

由此我們可以想到ThreadLocal類的實現,也是利用每個執行緒的獨立性把需要的資訊放入ThreadLocal,思想就是一種以空間換時間的策略(多個執行緒都有自己獨立的ThreadLocal儲存區,消耗了一定的空間,但是我們就不需要通過其他方式去儲存需要的資訊並獲取,時間上有很大的優化)

原始碼檔案寫道:

If not set,the default is the ClassLoader context of the parent Thread. The context ClassLoader of the primordial thread is typically set to the class loader used to load the application.複製程式碼

告訴我們如果的上下文類載入器沒有被設定,那麼預設值就是載入當前執行緒的類載入器,載入當前執行緒的類載入器就是載入該應用的類載入器,一般為系統類載入器

我們後面就根據一些原始碼分析和案例使用來看一看上下文類載入器到底有多麼強大的功能,竟然可以破壞雙親委派模型

SPI載入以及破壞雙親委派模型

SPI—Service Provider Interface,服務提供介面,像JDBC載入就是使用了spi,服務提供商使用spi擴充套件介面功能,類似根據jdk提供的一個介面不同服務提供商實現不同的介面實現,封裝成一個jar包,我們通過匯入這個jar包就可以使用服務提供商提供的該不同介面實現對應功能,通過ServiceLoader類載入不同服務提供商的實現—你可以簡單理解為策略模式

ServiceLoader

官方檔案寫的:是一個載入服務提供商提供的服務實現的裝置

A simple service-provider loading facility.複製程式碼

使用:官方檔案寫到:

A service provider is identified by placing a provider-configuration file in the resource directory META-INF/services. The file's name is the fully-qualified binary name of the service's type. The file contains a list of fully-qualified binary names of concrete provider classes,one per line. 複製程式碼

就是說服務提供商需要在提供的服務實現所在的resource目錄中編寫配置檔案,指定檔案目錄為META-INF/services,檔名是服務型別的全限定名(也就是jdk中服務介面的介面全限定名),用於尋找服務介面,檔案內容應該儲存服務介面實現類的全限定名,也就是該類在jar包中的包名+類名

如:JDBC->檔名:java.sql.Driver 檔案內容:com.mysql.cj.jdbc.Driver

JDK就會去找到java.sql.Driver這個介面,然後找到檔案內容中的在jar包中對應的com.mysql.cj.jdbc.Driver類作為該介面的實現

同一個服務的不同提供商將根據jdk SPI規範編寫符合規範的實現類(對類沒有要求,只需要實現介面就好了,但是需要新增META-INF/services/服務限定名檔案,在其中每一行寫服務提供商提供的類相應的在jar包目錄下的全限定名)

自定義SPI服務

下面我們自己實現一個spi服務看一下它到底是如何運作的,寫完之後我們再看原始碼

  • 首先我們編寫一個服務介面,介面包路徑全限定名top.dzou.jvm.spi
package top.dzou.jvm.spi;

public interface TestInterface {
    void saySomething();
}
複製程式碼

  • 再編寫兩個不同的介面服務實現,模擬不同服務提供商提供的不同實現,包路徑為top.dzou.jvm.spi.impl
package top.dzou.jvm.spi.impl;
public class ConcreteImpl1 implements TestInterface {
    @Override
    public void saySomething() {
        System.out.println("I am first service provider interface impl;");
    }
}複製程式碼

package top.dzou.jvm.spi.impl;
public class ConcreteImpl2 implements TestInterface {
    @Override
    public void saySomething() {
        System.out.println("I am second service provider interface impl;");
    }
}複製程式碼

  • 我們還需要編寫配置檔案,在classpath下的建立配置檔案目錄META-INF/services,配置檔名為介面包路徑全限定名top.dzou.jvm.spi.TestInterface`
top.dzou.jvm.spi.impl.ConcreteImpl1
top.dzou.jvm.spi.impl.ConcreteImpl2複製程式碼

  • 編寫一個測試類,使用ServiceLoader
public class TestSpi {
    public static void main(String[] args) {
        //Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader().getParent());
        ServiceLoader<TestInterface> loader = ServiceLoader.load(TestInterface.class);
        Iterator<TestInterface> iterator = loader.iterator();
        System.out.println("current class loaded by :"+TestSpi.class.getClassLoader());
        System.out.println("current thread loader :"+Thread.currentThread().getContextClassLoader());
        System.out.println("service interface loader :"+loader.getClass().getClassLoader());
        while(iterator.hasNext()){
            TestInterface next = iterator.next();
            next.saySomething();
        }
    }
}

輸出:
current class loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2
current thread loader :sun.misc.Launcher$AppClassLoader@18b4aac2
service interface loader :null
I am first service provider interface impl;
I am second service provider interface impl;複製程式碼

如果我們把main函式第一行之前加上一行

Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader().getParent());複製程式碼

輸出為

current class loaded by :sun.misc.Launcher$AppClassLoader@18b4aac2
current thread loader :sun.misc.Launcher$ExtClassLoader@266474c2
service interface loader :null複製程式碼

解釋:

你可以把我們寫的介面實現看成是某個服務商提供者編寫的jar包的類,把介面看成是JDK提供的服務介面,然後在jar包中的resource目錄下的META-INF/services中編寫了一個與JDK提供服務介面全限定名相同的配置檔案,在其中配置了兩個具體實現類的類全限定名,就可以通過ServiceLoader去使用這兩個類作為JDK介面的實現類,我們在測試類中測試的結果可以看到除了ServiceLoader類由啟動類載入器載入,執行緒和測試類都是通過系統類載入器載入的;

但是當我們設定了擴充套件類為執行緒上文文類載入器的時候,可以看到列印結果是我們自己編寫的服務介面實現沒有被載入,那這是為什麼?

答:很簡單,因為ServiceLoader是通過上下文類載入器獲取到系統類載入器的引用,通過系統類載入器來幫助我們實現訪問服務實現的類,但是現在我們的上下文類載入器為擴充套件類載入器,顯然擴充套件類載入器是載入和訪問不了我們自己編寫的服務實現類,所以自然沒有列印處載入的資訊,更沒有去呼叫方法

SPI原理以及ServiceLoader原始碼分析

我們通過上下文類載入器和自定義SPI實現大致已經知道SPI是怎麼運作的了,我們下面看一下它的原始碼

因為sun公司原始碼有些是不對外開放的,所以我們看一下反編譯的原始碼就好了,大致都能理解

  • 首先在ServiceLoader中有這樣一段程式碼
private static final String PREFIX = "META-INF/services/";複製程式碼

現在我們就可以看懂這是什麼了,為什麼服務提供商都要在jar包中在classpath目錄下編寫這麼一個目錄,就是一個絕對路徑,系統類載入器就是通過這個路徑去尋找jar包中的服務介面實現類

  • 我們再看一下自定義SPI實現的ServiceLoader.load()方法
public static <S> ServiceLoader<S> load(Class<S> var0) {
    ClassLoader var1 = Thread.currentThread().getContextClassLoader();//核心方法
    return load(var0,var1);
}複製程式碼

在load中ServiceLoader拿到了上下文類載入器,作為引數傳入load方法

private ServiceLoader(Class<S> var1,ClassLoader var2) {
        this.service = (Class)Objects.requireNonNull(var1,"Service interface cannot be null");
        this.loader = var2 == null ? ClassLoader.getSystemClassLoader() : var2;
        this.acc = System.getSecurityManager() != null ? AccessController.getContext() : null;
        this.reload();
    }複製程式碼

load方法返回了一個ServiceLoader物件,構造方法把loader設定為了剛剛拿到的當前執行緒上下文類載入器

  • 我們看一下使用loader的地方

ServiceLoader維護了一個內部類LazyIterator實現了Iterator介面作為使用服務提供商在配置檔案中編寫的所有服務實現類的迭代器,看一下hasNextService方法,我把關鍵部分留了下來

private boolean hasNextService() {
    //關鍵是這裡,反編譯把常量直接載入過來了
    if (this.configs == null) {
        try {
            String var1 = "META-INF/services/" + this.service.getName();//這裡service就是
            if (this.loader == null) {
                this.configs = ClassLoader.getSystemResources(var1);//一般不會來到這,如果出現異常來到這也要把loader設定為系統類載入器
            } else {
                this.configs = this.loader.getResources(var1);//使用系統類載入器根據jar包中路徑獲取資源,也就是使用服務實現
            }
        } catch (IOException var2) {
            ServiceLoader.fail(this.service,"Error locating configuration files",var2);
        }
           
//下面使用迭代器,負責判斷是否有其他服務實現
                while(this.pending == null || !this.pending.hasNext()) {
                    if (!this.configs.hasMoreElements()) {
                        return false;
                    }

                    this.pending = ServiceLoader.this.parse(this.service,(URL)this.configs.nextElement());
                }

                this.nextName = (String)this.pending.next();
                return true;
            }
        }複製程式碼

再看一下nextService()方法

private S nextService() {
                String var1 = this.nextName;//拿到下一個服務類的類全限定名
                this.nextName = null;
                Class var2 = null;
                try {
                    var2 = Class.forName(var1,false,this.loader);//使用反射載入服務實現,loader為系統類載入器,var1為nextName就是服務類全限定名
                    
                    Object var3 = this.service.cast(var2.newInstance());
                    ServiceLoader.this.providers.put(var1,var3);//載入成功放入Maop中
                    return var3;
                    }
        }複製程式碼

  • 我們看一下最根本的Launcher中的初始化方法,我們知道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);複製程式碼

它首先就是獲取系統類載入器作為Launcher中把儲存的loader引用,因為它是JDK最下面的類載入器。可以通過getParent方法獲取上冊載入器;並且呼叫了 Thread.currentThread().setContextClassLoader方法把系統類載入器設定為當前執行緒的上下文類載入器

SPI原理和ServiceLoader的原始碼講完我們下面看一下SPI對服務介面的實際使用

SPI—JDBC載入分析

我們一般通過Class.forName("com.mysql.cj.jdbc.Driver");先使用載入當前類的載入器(也就是系統類載入器)載入該classpath下的mysql驅動

現在我們再來看這張圖片就能會容易理解了,配置檔案的內容你可能也已經想到了,就是JDBC的mysql驅動

com.mysql.cj.jdbc.Driver或者com.mysql.jdbc.Driver

  • 我們看一下這個mysql的Driver類
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}複製程式碼

我們在通過Class.forName載入完該Driver時會自動初始化該類,就會執行static語句塊,自然就會載入引用的DriverManger,根據雙親委託模型,把載入DriverManager的任務交給啟動器類載入器

  • 載入完成後繼續執行上面static塊會執行registerDriver方法,自然就會先初始化DriverManager,執行下述DriverManager的static塊
static {
        loadInitialDrivers();
    }複製程式碼

  • loadInitialDrivers

我們看一下它靜態塊中執行的初始化Driver的方法

private static void loadInitialDrivers() {
        String var0 = (String)AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");//如果存在系統的jdbc driver則返回,一般不存在,需要載入
                }
            });
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader var1 = ServiceLoader.load(Driver.class);//ServiceLoader載入java.sql.Driver
                Iterator var2 = var1.iterator();
                while(var2.hasNext()) {//通過hasNext呼叫hasNextService方法拿取配置檔案中指定的類的資源
                    var2.next();//呼叫nextService方法會通過Class.forName()載入這個類
                }
            } 
                return null;
            }
        });
        if (var0 != null && !var0.equals("")) {//如果System.getProperty("jdbc.drivers");中有驅動
            String[] var1 = var0.split(":");
            String[] var2 = var1;
            int var3 = var1.length;
            for(int var4 = 0; var4 < var3; ++var4) {
                String var5 = var2[var4];
                println("DriverManager.Initialize: loading " + var5);
                Class.forName(var5,true,ClassLoader.getSystemClassLoader());//嘗試載入System.getProperty中的驅動
            }
        }
    }複製程式碼

這麼一看進行了很多次Class.forName()載入驅動,那我們為什麼還需要手動呼叫Class.forName("com.mysql.cj.jdbc.Driver");?是不是可以不手動呼叫這一步?

答案是可以的,我們手動呼叫這步是因為JDK以前還不支援這種做法,需要呼叫,但是後面版本的JDK中可以不需在呼叫這一句了,因為只要在classpath中,它就會在loadInitialDrivers中呼叫next中呼叫nextService方法中呼叫了這句Class.forName()

  • 載入了驅動後,下面我們再看一下它的獲取連線的方法,裡面還有與類載入有關的過程

String var0: 驅動類全限定名

Properties var1: 包含資料庫連線引數的配置資訊

Class var2: 反射拿到的呼叫getConnetion方法的類

關鍵程式碼如下

private static Connection getConnection(String var0,Properties var1,Class<?> var2) throws SQLException {
        ClassLoader var3 = var2 != null ? var2.getClassLoader() : null;//拿到載入呼叫類的類載入器,一般為系統類載入器
        Class var4 = DriverManager.class;
        synchronized(DriverManager.class) {
            if (var3 == null) {
                var3 = Thread.currentThread().getContextClassLoader();//如果不是系統類載入器就設定為當前執行緒的1類載入器,也就是儲存的系統類載入器的引用
            }
        } 
            Iterator var5 = registeredDrivers.iterator();
            while(true) {
                while(var5.hasNext()) {//呼叫迭代器來載入驅動
                    DriverInfo var6 = (DriverInfo)var5.next();
                    if (isDriverAllowed(var6.driver,var3)) {//關鍵在這裡
                        Connection var7 = var6.driver.connect(var0,var1);
                        if (var7 != null) {
                            return var7;
                        }
                    }
                }
            }
    }複製程式碼

  • isDriverAllowed方法

就是為了辨別驅動var0是否有var1(當前執行緒的類載入器、載入當前呼叫類的類載入器)所載入,也就是var0是否在var1類載入器的名稱空間中

出現這種情況的原因:

1.上下文類載入器被設定為了高層的類載入器而不是系統類載入器

2.執行緒被切換了,當前執行緒的上下文類載入器不是載入呼叫類的類載入器

不同的類載入器對應不同的名稱空間,這樣的話,上下文類載入器引用的類載入器無法載入該驅動,也就無法使用該驅動

private static boolean isDriverAllowed(Driver var0,ClassLoader var1) {
    boolean var2 = false;
    if (var0 != null) {
        Class var3 = null;
        try {
            var3 = Class.forName(var0.getClass().getName(),var1);
        } catch (Exception var5) {
            var2 = false;//如果異常發生,表示無法由var0載入var1,名稱空間不同
        }
        var2 = var3 == var0.getClass();//否則只需要判斷載入的類和var0驅動類是否是一個類
    }
    return var2;
}複製程式碼

Tomcat載入簡要分析

Web伺服器載入需求

  • 部署在同一個伺服器的兩個web應用程式使用的java類庫相互隔離,兩個不同的應用程式也可以依賴用一個第三方類庫的不用版本,所以一個類庫只能在一個應用程式中可見
  • 部署在一個伺服器上的兩個web應用可以共享Java類庫,10個依賴Spring,那麼10個應用都需要一個獨立的Spring?顯然是不需要的
  • 為了安全性,伺服器所使用的類庫應該與應用程式類庫隔離
  • 像JSP這種檔案,需要支援動態熱更新,JSP修改後無需重啟伺服器,只需要重新整理頁面就可以了

tomcat載入模型

我們在上述情況下思考一下雙親委託模型可以實現嗎?

顯然不行,所以tomcat建立了自己的一套載入模型,如下:

  1. common類載入器就是負責載入伺服器和應用程式都可以共享的類庫,如classpath下的lib目錄
  2. catalina類載入器負責載入伺服器獨立的類庫,為了安全性不與應用程式共享的類庫
  3. shared類載入器就負責載入應用程式之間共享的類庫,像是Spring這樣的
  4. WebApp類載入器載入單個應用程式獨立的類庫,對其他應用程式不可見,如webapp下類庫
  5. jsp類載入器負責jsp檔案載入成servlet類,它需要解決熱更新的問題

JSP檔案的熱更新載入

我們知道一般載入過程,建立一個JSP頁面,啟動伺服器時由載入器載入成servlet類位元組碼檔案,但是當你JSP內容修改了以後,就相當於類檔案被修改了,這個時候我們只能重新啟動應用程式來再次載入這個類來實現修改後的更新,但是如果是這樣的話就沒有人使用JSP

tomcat考慮到了這一點,提出了一種一個類載入器對應一個JSP檔案的實現方法

我們每次為JSP檔案載入建立一個特定的載入器,每個JSP就有一個類載入器,當我們在執行時發現JSP被修改了的話,我們就丟棄那個載入出來的Class檔案,通過重新建立一個新的JSP類載入器來載入更新的JSP檔案

為了實現不同應用程式隔離,伺服器和應用程式隔離,就不同在使用雙親委託模型,它會把所有載入交給父類,而保證每個類有且僅由一個,所以tomcat不得不破壞雙親委託模型,但它只是沒有遵循交給上層載入的規定,載入模型還是自上而下的

Tomcat決定把webapp目錄下的類由自己的WebappClassLoader載入,不委託給父類載入器,然後通過舞弊的上下文類載入器來實現父載入器對子類載入器載入的類的訪問與可見性