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

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

不只是爲了面試,還爲了從根本上學習和理解Java代碼的執行過程,提升本身對Java的理解java

Java虛擬機生命週期:mysql

  1. 程序正常結束
  2. 程序異常終止
  3. 操做系統錯誤
  4. System.exit()

類加載

添加idea屬性打印加載的類 -XX:+TraceClassLoadingweb

在Java代碼中,類的加載、鏈接和初始化都是在運行時完後的,每個類都經過類加載器加入加載到JVM中(堆中),造成一個虛擬機能夠直接使用的Java類型面試

1.加載

把Java字節碼加載成一段二進制流,讀取到內存,放在運行時數據區的方法區內;建立一個java.lang.Class對象描述該類的數據結構sql

能夠從磁盤、jar、war、網絡、本身編寫的class文件中加載class文件數據庫

2.鏈接

分爲驗證、準備、解析三個階段設計模式

(1)驗證

確保類加載的正確性,保證Class文件的字節流不會影響虛擬機的安全(由於class文件能夠從任何途徑生成),驗證失敗拋出VerifyError,驗證經過就把內存中的二進制流存放到JVM的運行時數據區的方法區中數組

  1. 文件格式驗證

文件開頭魔數表明JDK版本號等信息;常量池中是否有不支持的常量tomcat

只有驗證經過,二進制字節流纔會進入內存的方法區存儲安全

  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,0,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(), true, 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加載,不委託給父類加載器,而後經過舞弊的上下文類加載器來實現父加載器對子類加載器加載的類的訪問與可見性

相關文章
相關標籤/搜索