1. 程式人生 > >類載入器以及SPI

類載入器以及SPI

目錄:

           申明

1.載入

2.驗證

3.準備

4.解析

申明

本文很多的內容都是作者通過閱讀《深入理解Java虛擬機器[JVM高階特性與最佳實踐](周志明)》這本書學習到的。如果需要關於類載入更加詳細的細節資訊,請自行去閱讀JVM 相關書籍

JVM類載入以及SPI

類的生命週期:

說明:只有載入階段使用者可以通過自定義類載入器參與,別的階段都是虛擬機器自己主導的

1.載入

  1. 通過類的全限定名獲取描述此類的二進位制位元組流(獲取方式任意—這是一個開放式規範 比如:動態代理,網路傳輸,zip包中讀取比如jar,war。從資料庫中讀取……)
  2. 將位元組流所代表的靜態儲存結構儲存為方法區的執行時資料結構(1.8以後存在meta space,1.7 開始 類的靜態量以及 常量 都存在 head 區)
  3. 在java head 建立一個這個類的java.lang.Class 物件,作為訪問 方法區(1.8 meta space )的入口

說明:後面的所有載入操作我都統稱loading

2.驗證

  • 格式驗證:驗證位元組流是否是符合class檔案格式規範,是否能被當前虛擬機器處理
  1.   是否以魔數0xCAFEBABE開頭(這個是jvm 驗證的一個標準 )
  2.   主次版本號是否在當前虛擬機器的處理範圍(1.7的程式碼,你用1.6 jvm 編譯肯定編譯不了,這個就是驗證這類版本號的)
  3.   常量池的常量中是否有不被支援的常量型別(檢查常量的tag標誌)
  4.   Class檔案中各部分及檔案本身是否有被刪除或附加其他資訊
  5.   ……….
  • 元資料驗證:對位元組碼描述資訊進行語意解析,以保證其描述的資訊符合java語言規範要求
  1.   這個類是否有父類(除了Object所有類又應該有父類)
  2.   這個類是否繼承了不允許被繼承的類(比如final 修飾的類就不能被繼承)
  3.   如果這個類不是abstract 類,是否實現了父類或介面中要求實現的方法
  4.   ……..

提示:所有類最終父類都是Object

  • 位元組碼驗證:這是最複雜的一個階段,主要進行資料流和控制流分析。該階段將會對類的方法體進行分析,以保證被校驗類的方法在執行時不會做出危害虛擬機器安全的行為
  1.   保證方法體中的型別轉換是有效的,比如把一個父類賦值給子類,或則把一個類賦值給一個毫無關係的類,這樣的操作就是不合法的
  2.   保證任意時刻運算元棧的資料型別與指令程式碼序列都能配合工作,而不會出現像:在運算元棧放一個int資料,使用時卻按照long來載入進入區域性變量表中
  3.   ……
  • 符號引用驗證:這個階段的校驗發生在虛擬機器將符號引用轉換為直接引用的時候(也就是在解析階段的時候)
  1.   符號引用中通過字串描述的全限定名是否能找到對應的類
  2.   在指定類中是否存在符合方法的符號的欄位描述符以及簡單的名稱描述的方法和欄位
  3.   引用中的類、欄位、方法的訪問性(public,private…)是否能被當前類訪問

類載入器以及雙親委派模型

上面的載入階段的第一步,通過類的全限定名獲取描述此類的二進位制位元組流。這個動作,就是對使用者開放的一個動作,在這個階段java 平臺開發者 可以自定義自己的類載入器,方法是有很多種。

         這裡說一下 雙親委派模型 以及下面的SPI 規範的一種方式

雙親委派模型

  • 首先 從java 虛擬機器角度來說分為兩種類載入器:
  1.   啟動類載入器(Bootstrap ClassPoader)這個類載入器由C++實現,虛擬機器的一部分
  2.   其他所有的由java實現的類載入器
  • 從java 開發人員的角度來看 類載入器分得更細一些,大多數時候我們會使用到系統提供的三種類載入器:
  1.   啟動類載入器(Bootstrap ClassLoader)他負責載入<JAVA_HOME>/lib 目錄中的,或則被-Xbootclasspath引數所指定的路徑中被虛擬機器識別的的jar(比如rt.jar,名字不符合的及時放在lib 也不會被載入的) 到虛擬機器的記憶體中。

說明:啟動類載入器無法被java程式直接引用

  1.   擴充套件類載入器(Extension ClassLoader)這個由sun.misc.Launcher$ExtClassLoader 實現,它負責載入<JAVA_HOME>/lib/ext 下面的jar 。或則被java.ext.dirs 系統變數所指定的路徑中的所有類庫
  2.   應用類載入器(Application CLassLoader)這個sun.misc.Launcher$AppssLoader

實現,這個類的例項是ClassLoader.getSystemClassLoader()的返回值,所以也叫它系統類載入器,他負責載入classpath 下面的所有類,如果程式中沒有自定義的類載入器,一般就是使用的這個類載入器了

  1. 模型圖:

  • 雙親委派的作用是什麼呢?

保證類任意類載入環境的唯一性,比如java.lang.String 類 你可以再定義一個lava.lang.String 這樣的類,但是載入的時候不會被載入,並且會丟擲異常。

需要注意的時候 除了 Bootstrap ClassLoader 之外別的類載入器都應該有自己的父類載入器,這些類載入器的父子關係不是以繼承關係實現的,都是使用組合關係實現

從上圖su.msic.Launcher類中也可已看出他們兩並不是繼承的關係而是組合的關係

  • ExtClassLoader 和 AppClassLoader 都是定義在sun.msic.Launcher 裡面的static 內部 我們來看一下這兩個類的初始化過程
public class Launcher {

    private static URLStreamHandlerFactory factory = new Launcher.Factory();

    private static Launcher launcher = new Launcher();

    private static String bootClassPath = System.getProperty("sun.boot.class.path");

    private ClassLoader loader;

    private static URLStreamHandler fileHandler;



    public static Launcher getLauncher() {

        return launcher;

    }

    public Launcher() {
            

        Launcher.ExtClassLoader var1;

        try {
            //初始化 ExtClassLoader

            var1 = Launcher.ExtClassLoader.getExtClassLoader();

        } catch (IOException var10) {

            throw new InternalError("Could not create extension class loader", var10);

        }

        try {
            // 初始化 AppClassLoader

            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

        } catch (IOException var9) {

            throw new InternalError("Could not create application class loader", var9);

        }
            //設定執行緒上下文類載入器為 AppClassLoader

        Thread.currentThread().setContextClassLoader(this.loader);

             …    }       
ClassLoader.getSystemClassLoader() 最後呼叫到的就是這個類的這個方法,返回的是 AppClassLoader 例項 他是單例的
 public ClassLoader getClassLoader() {
            return this.loader;
       }
static class ExtClassLoader extends URLClassLoader {

    public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {

        final File[] var0 = getExtDirs();



        try {
          // 初始化 ExtClassLoader 例項

            return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {

                public Launcher.ExtClassLoader run() throws IOException {

                    int var1 = var0.length;

                    for(int var2 = 0; var2 < var1; ++var2) {

                        MetaIndex.registerDirectory(var0[var2]);

                    }

                    return new Launcher.ExtClassLoader(var0);

                }

            });

        } catch (PrivilegedActionException var2) {

            throw (IOException)var2.getException();

        }

    }



    public ExtClassLoader(File[] var1) throws IOException {

        super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);

        SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);

    }



    private static File[] getExtDirs() {

        String var0 = System.getProperty("java.ext.dirs");

        File[] var1;

        為了減少篇幅部分程式碼省略

        return var1;

    }

    為了減少篇幅部分程式碼省略

}
 static class AppClassLoader extends URLClassLoader {
    final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
    public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
       

final String var1 = System.getProperty("java.class.path");
        final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);

            初始化 AppClassLoader 例項
        return (ClassLoader)AccessController.doPrivileged(new        PrivilegedAction<Launcher.AppClassLoader>() {
            public Launcher.AppClassLoader run() {
                URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                return new Launcher.AppClassLoader(var1x, var0);
            }
        });
    }      
  • 自定義類載入器
public class ClassLoaderDemo {

    public Class definedMyslefClassLoader() throws ClassNotFoundException {
          這個就是自定義的累載入器,實際開發中請單獨定義一個類

    ClassLoader classLoader = new ClassLoader() {

        @Override

        protected Class<?> findClass(String name) throws ClassNotFoundException {

            String fileName = name.substring(name.lastIndexOf(".")+1)+".class";

            InputStream resourceAsStream = getClass().getResourceAsStream(fileName);

            byte [] b = new byte[0];

            try {

                b = new byte[resourceAsStream.available()];

                resourceAsStream.read(b);

            } catch (IOException e) {

                e.printStackTrace();

            }

            return defineClass(name,b,0,b.length);

        }

    };

    return classLoader.loadClass(ClassLoaderDemo.class.getName());

}

SPI (service provider interface)

SPI 是JDK 內建的服務發現機制,出現於雙親委派模式第二次被“破壞“時候,因為雙親委派模式雖然很好的解決了各類載入器的基礎類的統一問題,但是當你JDK定義的基礎類需要去呼叫(載入)對應的自適應性擴充套件的類的時候,雙親委派就無法實現了。

         簡單來說比如上面說的JDK定義的基礎類比如是一個定義好的介面,但是這個介面的實現不是由JDK 開發者來實現的,他們只是定義了這樣一個標準,具體的實現需要根據不廠商的不同情況各自去實現他的內容,這樣的話我怎麼去發現(載入)這些實現類呢?很顯然雙親委派無法實現,他只是用來保證基類的統一的。
    當然你可以掃描classpath 下面所有的class 與 jar 中的class 然後用ClassLoader 載入 ,然後再判斷是否是給定藉口的實現。但是 很明顯這是一種非常非常影響效能的實現。

針對這樣的問題,那麼我們肯定要去解決它,所以SPI 出現了。
    jdk 中提供了一個 ServiceLoader<T> 的一個類載入器(這個類載入器的原始碼很簡單這裡就不細說了,有興趣的可以自己去看看) 來實現這種服務發現 ,你只需要根據他的規定在 根目錄(Classpath)建立一個META-INF/service目錄。然後在下面建立一個以介面全路徑命名的檔案,在裡面寫上你的實現了的全路徑就可以了
如果是多個實現,那就分行寫(一行寫一個實現的全路徑名)

典型的例子是,JDBC,我想這個是每一個java 程式開發者都接觸過的東西,JDK 中定義了相關的Driver的Interface,相關的廠商來做對應的實現,比如Mysql 實現了自己的驅動,Oracel 也實現了自己的驅動,使用的時候 只需要我們載入對應的驅動就可以了。

為了證明我所說的,我準備了pgsql 驅動包的截圖:

以及java.sql.Driver介面的路徑截圖

很明顯了吧,Driver類是在rt.jar 中定義的service 介面,不同的資料庫廠商,會有他們對應的實現,而這些實現的感知就是通過jdk 定義的SPI 機制,這是一種定義規範。

         還有一個SPI的典型事例 要說一下 Alibaba 的dubbo 框架,dubbo 框架裡面的擴充套件實現都是基於SPI(dubbo 自定義的一種SPI 規範)來實現的

         Dubbo 的SPI 規範 定義了classpath下的三個路徑

META-INF/service/;META-INF/dubbo/; META-INF/dubbo/internal/

         這些擴充套件類的載入交給了 dubbo 中定義的 ExtensionLoader類來負責載入

        友情提示:Alibaba 的dubbo  現在交給apache 維護了

3.準備

  1. 該階段為類變數(static 修飾的)在head 區 分配並設定初始值。
  2. 物件引用預設為null , boolean 預設 false , int = 0 ,float = 0.0f….

private static int n = 100;

準備階段完成後 n = 0 , 而不是 100 , 在初始化階段 才會把 100 賦值給n

特殊情況:

private static final int n = 100;

常量值(final 修飾的值) 編譯之後會存在class 檔案的靜態常良池中。此時生成了一個constantValue。

在準備階段會根據這個值給n賦值

  1. 注意 : 例項變數 在物件例項化的時候分配

4.解析

解析階段是虛擬機器將常量池內的符號引用替換為直接引用的過程。

  • 符號引用:簡單理解 就是一個字串,比如引用一個類,java.lang.String,

這就是一個符號引用。

詳細的解釋如圖:

  1.  首先存在這樣一個類:

public class test {

    public static final String aaa = "AAA";

    public final String bbb = "BBB";

    public String ccc = "CCC";

    public static  String ddd = "DDD";

}

2.  反編譯以後:

  •  直接引用:指標或則記憶體偏移量地址。引用的物件一定在記憶體中已經載入

5.初始化階段

         在準備階段變數已經被賦過一次系統要求的初始值。初始化階段程式設計師會通過程式的主管計劃去初始化變數以及其他資源。

  • 執行類構造器<clinit>

<clinit>

  1. 編譯器收集類所有的變數的賦值動作和靜態語句塊(static{} 塊)中的語句合併產生的,收集順序是根據語句在原始檔的出現順序決定的,靜態語句塊只能訪問定義在它之前的變數,定義在它之後的變數它能夠賦值,但是不能訪問
  2. 它不是類的構造方法,他也不需要顯示的呼叫父類的構造器,虛擬機器會保證執行他之前,父類的<clinit>已經執行完成
  3. 如果一個累沒有對變數的賦值操作,沒有靜態語句快,那這個類就不會有<clinit>
  • <clinit> 的執行時執行緒安全的,多個執行緒要求初始化同一個類,只有一個執行緒能執行別的都會阻塞等待
  • 初始化場景:

虛擬機器規定了有且只有5種情況必須對類進行初始化

  1. 執行new , getstatic , putstatic 以及invokestatic指令的時候
  2. 使用reflect對類反射呼叫的時候
  3. 初始化一個類的時候,父類沒有初始化,這時候會先初始化父類
  4. 啟動虛擬機器時需要初始化包含main 方法的類

以下幾種情況不會觸發類的初始化

  • 子類訪問父類的static 欄位,只會觸發父類的初始化,而不會初始化子類

但是HotSpot 虛擬機器有執行子類的loading操作(有興趣的可以配置一下jvm

(-verbose:class)引數,看一下輸出的loading資訊

class parent {

    static int i = 10;

    static {

        System.out.println("初始化 parent ");

    }

}

class Children extends parent {

    static {

        System.out.println("初始化 children ");

    }

}

public class test {

    public static void main(String[] args) {

        System.out.println(Children.i);

    }

}

輸出結果:

初始化 parent

10
  • 定義物件陣列不會觸發物件的初始化 也不會有loading
public static void main(String[] args) {

    Parent[] parents = new Parent[10];

}

輸出結果:什麼都沒有 說明沒有 init 操作

  • 常量在編譯後會存入呼叫類的常良池中,本質上並沒有直接的引用關係,所以不會觸發定義的常量的初始化
 class Parent {

    static final int B = 100;

    static {

        System.out.println("初始化 Parent ");

    }

}

public class test {

    public static void main(String[] args) {

        System.out.println(Parent.B);

    }

}

輸出結果:這也證明了Parent 類沒有初始化,在HotSpot中甚至都沒有loading

100

下面我們看一下反編譯test.class出來的東西,從下圖可以看到編譯後的情況,直接將100這個值放在了test.class 檔案的常量池中

  • 通過類名.class獲取Class 物件,不會觸發類的初始化,但是會有loading 
 public class test {

    public static void main(String[] args) throws ClassNotFoundException {

        Class clazz =  Parent.class;

        System.out.println(clazz);

    }

}

輸出結果:從輸出的類載入資訊 也可以看出 是先觸發了loading 動作

[Loaded com.Parent from file:/D:/IDEA_SPACE/stu/target/classes/]

class com.Parent
  • Class.forName獲取Class 物件是否會有初始化操作。
 public class test {



    public static void main(String[] args) throws ClassNotFoundException {

        Class<?> aClass = Class.forName("com.Parent");

        System.out.println(aClass);

    }

}

輸出結果:很明顯從輸出資訊就能看出 這種方式是有初始化動作的

[Loaded com.Parent from file:/D:/IDEA_SPACE/stu/target/classes/]

初始化 Parent

class com.Parent

看一下原始碼:第二個引數forname0這個方法的第二個引數為true,也就是說載入之後進行初始化操作

@CallerSensitive

public static Class<?> forName(String className)

            throws ClassNotFoundException {

    Class<?> caller = Reflection.getCallerClass();

    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);

}

下面這種方式不會觸發初始化

Class<?> aClass = Class.forName("com.Parent", false, Parent.class.getClassLoader());
  • 通過ClassLoader的loadClass 方法,也不會觸發初始化這個動作
 public class test {



    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {

        Class<?> aClass = new ClassLoader(){}.loadClass("com.Parent");

        System.out.println(aClass);

    }

}

相關推薦

java載入以及spi

類載入器概述: 每個編寫的”.java”拓展名類檔案都儲存著需要執行的程式邏輯,這些”.java”檔案經過Java編譯器編譯成拓展名為”.class”的檔案,”.class”檔案中儲存著Java程式碼經轉換後的虛擬機器指令,當需要使用某個類時,虛擬機器將會載入它的”.class”檔案,並

載入以及SPI

目錄:            申明 1.載入 2.驗證 3.準備 4.解析 申明 本文很多的內容都是作者通過閱讀《深入理解Java虛擬機器[JVM高階特性與最佳實踐](周志明)》這本書學習到的。如果需要關於類載入更加詳細的細節資訊,請自行去閱

Java執行緒上下文載入SPI

執行緒上下文類載入器(context class loader)是從JDK 1.2開始引入的。類 java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoader cl)用來獲取和設定

【深入理解Java虛擬機器 】載入的名稱空間以及的解除安裝

類載入器的名稱空間 每個類載入器又有一個名稱空間,由其以及其父載入器組成 類載入器的名稱空間的作用和影響 每個類載入器又有一個名稱空間,由其以及其父載入器組成 在每個類載入器自己的名稱空間中不能出現相同類名的類 (此處值得是類的全名,包含包名) 在不同的類名稱空間中,可能會出現多個相同的類名的類 如下

java載入——ClassLoader

web rac rgb 好的 全盤負責機制 安全 trac 字節 如何 Java的設計初衷是主要面向嵌入式領域,對於自己定義的一些類,考慮使用依需求載入原則。即在程序使用到時才載入類,節省內存消耗,這時就可以通過類載入器來動態載入。 假設你平時僅僅是做web開發,那應該

Java載入 ClassLoader的解析

index html dir obj ble body 6.4 odin 普通 //參考 : http://www.ibm.com/developerworks/cn/java/j-lo-classloader/ 類載入器基本概念 類載

finalkeyword對JVM載入的影響

public fin port args stat gpo sys tint () 眾所周知,當訪問一個類的變量或方法的時候。假設沒有初始化該類。就會先去初始化一個類 可是,當這個類的變量為final的時候,就

JAVA載入詳解

Java類載入器的作用就是在執行時載入類。Java類載入器基於三個機制:委託、可見性和單一性。委託機制是指將載入一個類的請求交給父類載入器,如果這個父類載入器不能夠找到或者載入這個類,那麼再載入它。可見性的原理是子類的載入器可以看見所有的父類載入器載入的類,而父類載入器看不到子類載入器載入的

1.java載入

Java類載入器ClassLoader總結 JAVA類裝載方式,有兩種: 1.隱式裝載, 程式在執行過程中當碰到通過new 等方式生成物件時,隱式呼叫類裝載器載入對應的類到jvm中。 2.顯式裝載, 通過class.forname()等方法,顯式載入需要的類 類載

sprinbboot 熱部署 造成載入 不一致問題

這裡只說devtools的方式,注意以下的熱部署方式在IDEA是預設沒有開啟自動編譯的,手動編譯需要快捷鍵(Ctrl+Shift+F9), 自動編譯的修改配置如下:(注意重新整理不要太快,會有1-2秒延遲) File-Settings-Compiler-Build Project automatic

Java載入(死磕5)

Java類載入器(  CLassLoader )  死磕5:  自定義一個檔案系統classLoader 本小節目錄 5.1. 自定義類載入器的基本流程 5.2. 入門案例:自定義檔案系統類載入器 5.3. 案例的環境配置 5.4 FileClassLoader

Java載入( 死磕9)

【正文】Java類載入器(  CLassLoader ) 死磕9:  上下文載入器原理和案例 本小節目錄 9.1. 父載入器不能訪問子載入器的類 9.2. 一個寵物工廠介面 9.3. 一個寵物工廠管理類 9.4 APPClassLoader不能訪問子載入器中的類 9.5. 執行緒上下文

Java載入( 死磕7)

【正文】Java類載入器(  CLassLoader )死磕7:  基於加密的自定義網路載入器 本小節目錄 7.1. 加密傳輸Server端的原始碼 7.2. 加密傳輸Client端的原始碼 7.3. 使用亦或實現簡單加密和解密演算法 7. 網路加密SafeClassLoader的原始

Java載入( 死磕 4)

【正文】Java類載入器(  CLassLoader ) 死磕 之4:  神祕的雙親委託機制 本小節目錄 4.1. 每個類載入器都有一個parent父載入器 4.2. 類載入器之間的層次關係 4.3. 類的載入次序 4.4 雙親委託機制原理與沙箱機制 4.5. forName

Java載入(死磕3)

【正文】Java類載入器(  CLassLoader ) 死磕3:  揭祕 ClassLoader抽象基類 本小節目錄 3.1. 類的載入分類:隱式載入和顯示載入 3.2. 載入一個類的五步工作 3.3. 如何獲取類的載入器 3.4 解刨載入器——ClassLoade

Java載入(死磕 1-2)

  Java類載入器(  CLassLoader ) 死磕 1、2:  匯入 & 類載入器分類 本小節目錄 1.匯入 1.1. 從class檔案的載入開始 1.2. 什麼是類載入器 2. JAVA類載入器分類 2.1. 作業系統的環境變數 2.2. Bo

Java載入( 深磕8)

【正文】Java類載入器(  CLassLoader ) 深磕 8:  使用ASM,和類載入器實現AOP 本小節目錄 8.1. ASM位元組碼操作框架簡介 8.2. ASM和訪問者模式 8.3. 用於增強位元組碼的事務類 8.4 通過ASM訪問註解 8.5. 通過ASM注入

Java載入( 深磕9)

【正文】Java類載入器(  CLassLoader ) 深磕9:  上下文載入器原理和案例 本小節目錄 9.1. 父載入器不能訪問子載入器的類 9.2. 一個寵物工廠介面 9.3. 一個寵物工廠管理類 9.4 APPClassLoader不能訪問子載入器中的類 9.5.

Java載入( 深磕7)

【正文】Java類載入器(  CLassLoader )深磕7:  基於加密的自定義網路載入器 本小節目錄 7.1. 加密傳輸Server端的原始碼 7.2. 加密傳輸Client端的原始碼 7.3. 使用亦或實現簡單加密和解密演算法 7. 網路加密SafeClassLoa

Java載入( 深磕 6)

【正文】Java類載入器(  CLassLoader )深磕 6:  自定義網路類載入器 本小節目錄 6.1. 自定義網路類載入器的類設計 6.2. 檔案傳輸Server端的原始碼 6.3. 檔案傳輸Client端的原始碼 6. 4 自定義載入器SocketClassLoade