日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

ccs加载out文件_类加载流程、类加载机制及自定义类加载器详解

發布時間:2023/12/4 编程问答 32 豆豆
生活随笔 收集整理的這篇文章主要介紹了 ccs加载out文件_类加载流程、类加载机制及自定义类加载器详解 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原文:juejin.im/post/5cffa528e51d4556da53d091

一、引言

當程序使用某個類時,如果該類還未被加載到內存中,則JVM會通過加載、鏈接、初始化三個步驟對該類進行類加載。

二、類的加載、鏈接、初始化

1、加載

類加載指的是將類的class文件讀入內存,并為之創建一個java.lang.Class對象。類的加載過程是由類加載器來完成,類加載器由JVM提供。我們開發人員也可以通過繼承ClassLoader來實現自己的類加載器。

1.1、加載的class來源
  • 從本地文件系統內加載class文件

  • 從JAR包加載class文件

  • 通過網絡加載class文件

  • 把一個java源文件動態編譯,并執行加載。

2、類的鏈接

通過類的加載,內存中已經創建了一個Class對象。鏈接負責將二進制數據合并到 JRE中。鏈接需要通過驗證、準備、解析三個階段。

2.1、驗證

驗證階段用于檢查被加載的類是否有正確的內部結構,并和其他類協調一致。即是否滿足java虛擬機的約束。

2.2、準備

類準備階段負責為類的類變量分配內存,并設置默認初始值。

2.3、解析

我們知道,引用其實對應于內存地址。思考這樣一個問題,在編寫代碼時,使用引用,方法時,類知道這些引用方法的內存地址嗎?顯然是不知道的,因為類還未被加載到虛擬機中,你無法獲得這些地址。

舉例來說,對于一個方法的調用,編譯器會生成一個包含目標方法所在的類、目標方法名、接收參數類型以及返回值類型的符號引用,來指代要調用的方法。

解析階段的目的,就是將這些符號引用解析為實際引用。如果符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那么解析將觸發這個類的加載(但未必會觸發解析與初始化)。

3、類的初始化

類的初始化階段,虛擬機主要對類變量進行初始化。虛擬機調用< clinit>方法,進行類變量的初始化。

java類中對類變量進行初始化的兩種方式:

  • 在定義時初始化

  • 在靜態初始化塊內初始化

  • 3.1、< clinit>方法相關

    虛擬機會收集類及父類中的類變量及類方法組合為< clinit>方法,根據定義的順序進行初始化。虛擬機會保證子類的< clinit>執行之前,父類的< clinit>方法先執行完畢。

    因此,虛擬機中第一個被執行完畢的< clinit>方法肯定是java.lang.Object方法。

    public?class?Test?{
    ????static?int?A?=?10;
    ????static?{
    ????????A?=?20;
    ????}
    }

    class?Test1?extends?Test?{
    ????private?static?int?B?=?A;
    ????public?static?void?main(String[]?args)?{
    ????????System.out.println(Test1.B);
    ????}
    }
    //輸出結果
    //20

    從輸出中看出,父類的靜態初始化塊在子類靜態變量初始化之前初始化完畢,所以輸出結果是20,不是10。

    如果類或者父類中都沒有靜態變量及方法,虛擬機不會為其生成< clinit>方法。

    接口與類不同的是,執行接口的<clinit>方法不需要先執行父接口的<clinit>方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也一樣不會執行接口的<clinit>方法。

    public?interface?InterfaceInitTest?{
    ????long?A?=?CurrentTime.getTime();

    }

    interface?InterfaceInitTest1?extends?InterfaceInitTest?{
    ????int?B?=?100;
    }

    class?InterfaceInitTestImpl?implements?InterfaceInitTest1?{
    ????public?static?void?main(String[]?args)?{
    ????????System.out.println(InterfaceInitTestImpl.B);
    ????????System.out.println("---------------------------");
    ????????System.out.println("當前時間:"+InterfaceInitTestImpl.A);
    ????}
    }

    class?CurrentTime?{
    ????static?long?getTime()?{
    ????????System.out.println("加載了InterfaceInitTest接口");
    ????????return?System.currentTimeMillis();
    ????}
    }
    //輸出結果
    //100
    //---------------------------
    //加載了InterfaceInitTest接口
    //當前時間:1560158880660

    從輸出驗證了:對于接口,只有真正使用父接口的類變量才會真正的加載父接口。這跟普通類加載不一樣。

    虛擬機會保證一個類的< clinit>方法在多線程環境中被正確地加鎖和同步,如果多個線程同時去初始化一個類,那么只有一個線程去執行這個類的< clinit>方法,其他線程都需要阻塞等待,直到活動線程執行< clinit>方法完畢。

    public?class?MultiThreadInitTest?{
    ????static?int?A?=?10;
    ????static?{
    ???????????System.out.println(Thread.currentThread()+"init?MultiThreadInitTest");
    ????????try?{
    ????????????TimeUnit.SECONDS.sleep(10);
    ????????}?catch?(InterruptedException?e)?{
    ????????????e.printStackTrace();
    ????????}
    ????}

    ????public?static?void?main(String[]?args)?{
    ????????Runnable?runnable?=?()?->?{
    ????????????System.out.println(Thread.currentThread()?+?"start");
    ????????????System.out.println(MultiThreadInitTest.A);
    ????????????System.out.println(Thread.currentThread()?+?"run?over");
    ????????};
    ????????Thread?thread1?=?new?Thread(runnable);
    ????????Thread?thread2?=?new?Thread(runnable);
    ????????thread1.start();
    ????????thread2.start();
    ????}
    }
    //輸出結果
    //Thread[main,5,main]init?MultiThreadInitTest
    //Thread[Thread-0,5,main]start
    //10
    //Thread[Thread-0,5,main]run?over
    //Thread[Thread-1,5,main]start
    //10
    //Thread[Thread-1,5,main]run?over

    從輸出中看出驗證了:只有第一個線程對MultiThreadInitTest進行了一次初始化,第二個線程一直阻塞等待等第一個線程初始化完畢。

    3.2、類初始化時機
  • 當虛擬機啟動時,初始化用戶指定的主類;

  • 當遇到用以新建目標類實例的new指令時,初始化new指令的目標類;

  • 當遇到調用靜態方法或者使用靜態變量,初始化靜態變量或方法所在的類;

  • 子類初始化過程會觸發父類初始化;

  • 如果一個接口定義了default方法,那么直接實現或者間接實現該接口的類的初始化,會觸發該接口初始化;

  • 使用反射API對某個類進行反射調用時,初始化這個類;

  • Class.forName()會觸發類的初始化

  • 3.3、final定義的初始化

    注意:對于一個使用final定義的常量,如果在編譯時就已經確定了值,在引用時不會觸發初始化,因為在編譯的時候就已經確定下來,就是“宏變量”。如果在編譯時無法確定,在初次使用才會導致初始化。

    public?class?StaticInnerSingleton?{
    ????/**
    ?????*?使用靜態內部類實現單例:
    ?????* 1:線程安全
    ?????* 2:懶加載
    ?????* 3:非反序列化安全,即反序列化得到的對象與序列化時的單例對象不是同一個,違反單例原則
    ?????*/
    ????private?static?class?LazyHolder?{
    ????????private?static?final?StaticInnerSingleton?INNER_SINGLETON?=?new?StaticInnerSingleton();
    ????}

    ????private?StaticInnerSingleton()?{
    ????}

    ????public?static?StaticInnerSingleton?getInstance()?{
    ????????return?LazyHolder.INNER_SINGLETON;
    ????}
    }

    看這個例子,單例模式靜態內部類實現方式。我們可以看到單例實例使用final定義,但在編譯時無法確定下來,所以在第一次使用StaticInnerSingleton.getInstance()方法時,才會觸發靜態內部類的加載,也就是延遲加載。

    這里想指出,如果final定義的變量在編譯時無法確定,則在使用時還是會進行類的初始化。

    3.4、ClassLoader只會對類進行加載,不會進行初始化
    public?class?Tester?{
    ????static?{
    ????????System.out.println("Tester類的靜態初始化塊");
    ????}
    }

    class?ClassLoaderTest?{
    ????public?static?void?main(String[]?args)?throws?ClassNotFoundException?{
    ????????ClassLoader?classLoader?=?ClassLoader.getSystemClassLoader();
    ????????//下面語句僅僅是加載Tester類
    ????????classLoader.loadClass("loader.Tester");
    ????????System.out.println("系統加載Tester類");
    ????????//下面語句才會初始化Tester類
    ????????Class.forName("loader.Tester");
    ????}
    }
    //輸出結果
    //系統加載Tester類
    //Tester類的靜態初始化塊

    從輸出證明:ClassLoader只會對類進行加載,不會進行初始化;使用Class.forName()會強制導致類的初始化。

    三、類加載器

    類加載器負責將.class文件(不管是jar,還是本地磁盤,還是網絡獲取等等)加載到內存中,并為之生成對應的java.lang.Class對象。一個類被加載到JVM中,就不會第二次加載了。

    那怎么判斷是同一個類呢?

    每個類在JVM中使用全限定類名(包名+類名)與類加載器聯合為唯一的ID,所以如果同一個類使用不同的類加載器,可以被加載到虛擬機,但彼此不兼容。

    1、JVM類加載器分類

    1.1、Bootstrap ClassLoader

    Bootstrap ClassLoader為根類加載器,負責加載java的核心類庫。根加載器不是ClassLoader的子類,是有C++實現的。

    public?class?BootstrapTest?{
    ????public?static?void?main(String[]?args)?{
    ????????//獲取根類加載器所加載的全部URL數組
    ????????URL[]?urLs?=?Launcher.getBootstrapClassPath().getURLs();
    ????????Arrays.stream(urLs).forEach(System.out::println);
    ????}
    }
    //輸出結果
    //file:/C:/SorftwareInstall/java/jdk/jre/lib/resources.jar
    //file:/C:/SorftwareInstall/java/jdk/jre/lib/rt.jar
    //file:/C:/SorftwareInstall/java/jdk/jre/lib/sunrsasign.jar
    //file:/C:/SorftwareInstall/java/jdk/jre/lib/jsse.jar
    //file:/C:/SorftwareInstall/java/jdk/jre/lib/jce.jar
    //file:/C:/SorftwareInstall/java/jdk/jre/lib/charsets.jar
    //file:/C:/SorftwareInstall/java/jdk/jre/lib/jfr.jar
    //file:/C:/SorftwareInstall/java/jdk/jre/classes

    根類加載器負責加載%JAVA_HOME%/jre/lib下的jar包(以及由虛擬機參數 -Xbootclasspath 指定的類)。

    我們將rt.jar解壓,可以看到我們經常使用的類庫就在這個jar包中。

    1.2 、Extension ClassLoader

    Extension ClassLoader為擴展類加載器,負責加載%JAVA_HOME%/jre/ext或者java.ext.dirs系統熟悉指定的目錄的jar包。大家可以將自己寫的工具包放到這個目錄下,可以方便自己使用。

    1.3、 System ClassLoader

    System ClassLoader為系統(應用)類加載器,負責加載加載來自java命令的-classpath選項、java.class.path系統屬性,或者CLASSPATH環境變量所指定的JAR包和類路徑。程序可以通過ClassLoader.getSystemClassLoader()來獲取系統類加載器。如果沒有特別指定,則用戶自定義的類加載器默認都以系統類加載器作為父加載器。

    四、類加載機制

    1.1、JVM主要的類加載機制。

  • 全盤負責:當一個類加載器負責加載某個Class時,該Class所依賴和引用的其他Class也由該類加載器負責載入,除非顯示使用另一個類加載器來載入。

  • 父類委托(雙親委派):先讓父加載器試圖加載該Class,只有在父加載器無法加載時該類加載器才會嘗試從自己的類路徑中加載該類。

  • 緩存機制:緩存機制會將已經加載的class緩存起來,當程序中需要使用某個Class時,類加載器先從緩存區中搜尋該Class,只有當緩存中不存在該Class時,系統才會讀取該類的二進制數據,并將其轉換為Class對象,存入緩存中。這就是為什么更改了class后,需要重啟JVM才生效的原因。

  • 注意:類加載器之間的父子關系并不是類繼承上的父子關系,而是實例之間的父子關系。

    public?class?ClassloaderPropTest?{
    ????public?static?void?main(String[]?args)?throws?IOException?{
    ????????//獲取系統類加載器
    ????????ClassLoader?systemClassLoader?=?ClassLoader.getSystemClassLoader();
    ????????System.out.println("系統類加載器:"?+?systemClassLoader);
    ????????/*
    ????????獲取系統類加載器的加載路徑——通常由CLASSPATH環境變量指定,如果操作系統沒有指定
    ????????CLASSPATH環境變量,則默認以當前路徑作為系統類加載器的加載路徑
    ?????????*/
    ????????Enumeration?eml?=?systemClassLoader.getResources("");while?(eml.hasMoreElements())?{
    ????????????System.out.println(eml.nextElement());
    ????????}//獲取系統類加載器的父類加載器,得到擴展類加載器
    ????????ClassLoader?extensionLoader?=?systemClassLoader.getParent();
    ????????System.out.println("系統類的父加載器是擴展類加載器:"?+?extensionLoader);
    ????????System.out.println("擴展類加載器的加載路徑:"?+?System.getProperty("java.ext.dirs"));
    ????????System.out.println("擴展類加載器的parant:"?+?extensionLoader.getParent());
    ????}
    }//輸出結果//系統類加載器:sun.misc.Launcher$AppClassLoader@18b4aac2//file:/C:/ProjectTest/FengKuang/out/production/FengKuang///系統類的父加載器是擴展類加載器:sun.misc.Launcher$ExtClassLoader@1540e19d//擴展類加載器的加載路徑:C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext//擴展類加載器的parant:null

    從輸出中驗證了:系統類加載器的父加載器是擴展類加載器。但輸出中擴展類加載器的父加載器是null,這是因為父加載器不是java實現的,是C++實現的,所以獲取不到。但擴展類加載器的父加載器是根加載器。

    1.2、類加載流程圖

    圖中紅色部分,可以是我們自定義實現的類加載器來進行加載。

    五、創建并使用自定義類加載器

    1、自定義類加載分析

    除了根類加載器,所有類加載器都是ClassLoader的子類。所以我們可以通過繼承ClassLoader來實現自己的類加載器。

    ClassLoader類有兩個關鍵的方法:

  • protected Class?loadClass(String name, boolean resolve):name為類名,resove如果為true,在加載時解析該類。

  • protected Class?findClass(String name)?:根據指定類名來查找類。

  • 所以,如果要實現自定義類,可以重寫這兩個方法來實現。但推薦重寫findClass方法,而不是重寫loadClass方法,因為loadClass方法內部會調用findClass方法。

    我們來看一下loadClass的源碼

    protected?Class>?loadClass(String?name,?boolean?resolve)
    ????????throws?ClassNotFoundException
    ????{
    ????????synchronized?(getClassLoadingLock(name))?{
    ????????????//第一步,先從緩存里查看是否已經加載
    ????????????Class>?c?=?findLoadedClass(name);
    ????????????if?(c?==?null)?{
    ????????????????long?t0?=?System.nanoTime();
    ????????????????try?{
    ????????????????//第二步,判斷父加載器是否為null
    ????????????????????if?(parent?!=?null)?{
    ????????????????????????c?=?parent.loadClass(name,?false);
    ????????????????????}?else?{
    ????????????????????????c?=?findBootstrapClassOrNull(name);
    ????????????????????}
    ????????????????}?catch?(ClassNotFoundException?e)?{
    ????????????????????//?ClassNotFoundException?thrown?if?class?not?found
    ????????????????????//?from?the?non-null?parent?class?loader
    ????????????????}

    ????????????????if?(c?==?null)?{
    ???????????????????//第三步,如果前面都沒有找到,就會調用findClass方法
    ????????????????????long?t1?=?System.nanoTime();
    ????????????????????c?=?findClass(name);

    ????????????????????//?this?is?the?defining?class?loader;?record?the?stats
    ????????????????????sun.misc.PerfCounter.getParentDelegationTime().addTime(t1?-?t0);
    ???????????????????sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
    ????????????????????sun.misc.PerfCounter.getFindClasses().increment();
    ????????????????}
    ????????????}
    ????????????if?(resolve)?{
    ????????????????resolveClass(c);
    ????????????}
    ????????????return?c;
    ????????}
    ????}

    loadClass加載方法流程:

  • 判斷此類是否已經加載;

  • 如果父加載器不為null,則使用父加載器進行加載;反之,使用根加載器進行加載;

  • 如果前面都沒加載成功,則使用findClass方法進行加載。

  • 所以,為了不影響類的加載過程,我們重寫findClass方法即可簡單方便的實現自定義類加載。

    2、實現自定義類加載器

    基于以上分析,我們簡單重寫findClass方法進行自定義類加載。

    public?class?Hello?{
    ???public?void?test(String?str){
    ???????System.out.println(str);
    ???}
    }

    public?class?MyClassloader?extends?ClassLoader?{

    ????/**
    ?????*?讀取文件內容
    ?????*
    ?????*?@param?fileName?文件名
    ?????*?@return
    ?????*/
    ????private?byte[]?getBytes(String?fileName)?throws?IOException?{
    ????????File?file?=?new?File(fileName);
    ????????long?len?=?file.length();
    ????????byte[]?raw?=?new?byte[(int)?len];
    ????????try?(FileInputStream?fin?=?new?FileInputStream(file))?{
    ????????????//一次性讀取Class文件的全部二進制數據
    ????????????int?read?=?fin.read(raw);
    ????????????if?(read?!=?len)?{
    ????????????????throw?new?IOException("無法讀取全部文件");
    ????????????}
    ????????????return?raw;
    ????????}
    ????}

    ????@Override
    ????protected?Class>?findClass(String?name)?throws?ClassNotFoundException?{
    ????????Class?clazz?=?null;
    ????????//將包路徑的(.)替換為斜線(/)
    ????????String?fileStub?=?name.replace(".",?"/");
    ????????String?classFileName?=?fileStub?+?".class";
    ????????File?classFile?=?new?File(classFileName);

    ????????//如果Class文件存在,系統負責將該文件轉換為Class對象
    ????????if?(classFile.exists())?{
    ????????????try?{
    ????????????????//將Class文件的二進制數據讀入數組
    ????????????????byte[]?raw?=?getBytes(classFileName);
    ????????????????//調用ClassLoader的defineClass方法將二進制數據轉換為Class對象
    ????????????????clazz?=?defineClass(name,?raw,?0,?raw.length);
    ????????????}?catch?(IOException?e)?{
    ????????????????e.printStackTrace();
    ????????????}
    ????????}
    ????????//如果clazz為null,表明加載失敗,拋出異常
    ????????if?(null?==?clazz)?{
    ????????????throw?new?ClassNotFoundException(name);
    ????????}
    ????????return?clazz;
    ????}

    ????public?static?void?main(String[]?args)?throws?Exception?{
    ????????String?classPath?=?"loader.Hello";
    ????????MyClassloader?myClassloader?=?new?MyClassloader();
    ????????Class>?aClass?=?myClassloader.loadClass(classPath);
    ????????Method?main?=?aClass.getMethod("test",?String.class);
    ????????System.out.println(main);
    ????????main.invoke(aClass.newInstance(),?"Hello?World");
    ????}
    }
    //輸出結果
    //Hello?World

    ClassLoader還有一個重要的方法defineClass(String name, byte[] b, int off, int len)。此方法的作用是將class的二進制數組轉換為Calss對象。

    此例子很簡單,我寫了一個Hello測試類,并且編譯過后放在了當前路徑下(大家可以在findClass中加入判斷,如果沒有此文件,可以嘗試查找.java文件,并進行編譯得到.class文件;或者判斷.java文件的最后更新時間大于.class文件最后更新時間,再進行重新編譯等邏輯)。

    六、總結

    本篇從類加載的三大階段:加載、鏈接、初始化開始細說每個階段的過程;詳細講解了JVM常用的類加載器的區別與聯系,以及類加載機制流程,最后通過自定義的類加載器例子結束本篇。

    【面試題專欄】

    2019年面試官最喜歡問的28道ZooKeeper面試題

    2019年全網最熱門的123個Java并發面試題總結

    全網最熱門的119個Spring問題,哪些你還不會?

    2020面試還搞不懂MyBatis?看看這27道面試題!(含答案和思維導圖)

    2020年去一線大廠面試先過SSM框架這一關!

    Spring Cloud+Spring Boot高頻面試題解析

    2019年常見的Linux面試題及答案解析,哪些你還不會?

    2019年常見Elasticsearch面試題答案解析

    18道kafka高頻面試題哪些你還不會?(含答案和思維導圖)

    2019年12道RabbitMQ高頻面試題你都會了嗎?(含答案解析)

    2019年Dubbo你掌握的如何?快看看這30道高頻面試題!

    2019年228道Java中高級面試題(8),哪些你還不會?

    ?你在看嗎?

    總結

    以上是生活随笔為你收集整理的ccs加载out文件_类加载流程、类加载机制及自定义类加载器详解的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。