ccs加载out文件_类加载流程、类加载机制及自定义类加载器详解
原文:juejin.im/post/5cffa528e51d4556da53d091
一、引言
當程序使用某個類時,如果該類還未被加載到內(nèi)存中,則JVM會通過加載、鏈接、初始化三個步驟對該類進行類加載。
二、類的加載、鏈接、初始化
1、加載
類加載指的是將類的class文件讀入內(nèi)存,并為之創(chuàng)建一個java.lang.Class對象。類的加載過程是由類加載器來完成,類加載器由JVM提供。我們開發(fā)人員也可以通過繼承ClassLoader來實現(xiàn)自己的類加載器。
1.1、加載的class來源
從本地文件系統(tǒng)內(nèi)加載class文件
從JAR包加載class文件
通過網(wǎng)絡加載class文件
把一個java源文件動態(tài)編譯,并執(zhí)行加載。
2、類的鏈接
通過類的加載,內(nèi)存中已經(jīng)創(chuàng)建了一個Class對象。鏈接負責將二進制數(shù)據(jù)合并到 JRE中。鏈接需要通過驗證、準備、解析三個階段。
2.1、驗證
驗證階段用于檢查被加載的類是否有正確的內(nèi)部結構,并和其他類協(xié)調(diào)一致。即是否滿足java虛擬機的約束。
2.2、準備
類準備階段負責為類的類變量分配內(nèi)存,并設置默認初始值。
2.3、解析
我們知道,引用其實對應于內(nèi)存地址。思考這樣一個問題,在編寫代碼時,使用引用,方法時,類知道這些引用方法的內(nèi)存地址嗎?顯然是不知道的,因為類還未被加載到虛擬機中,你無法獲得這些地址。
舉例來說,對于一個方法的調(diào)用,編譯器會生成一個包含目標方法所在的類、目標方法名、接收參數(shù)類型以及返回值類型的符號引用,來指代要調(diào)用的方法。
解析階段的目的,就是將這些符號引用解析為實際引用。如果符號引用指向一個未被加載的類,或者未被加載類的字段或方法,那么解析將觸發(fā)這個類的加載(但未必會觸發(fā)解析與初始化)。
3、類的初始化
類的初始化階段,虛擬機主要對類變量進行初始化。虛擬機調(diào)用< clinit>方法,進行類變量的初始化。
java類中對類變量進行初始化的兩種方式:
在定義時初始化
在靜態(tài)初始化塊內(nèi)初始化
3.1、< clinit>方法相關
虛擬機會收集類及父類中的類變量及類方法組合為< clinit>方法,根據(jù)定義的順序進行初始化。虛擬機會保證子類的< clinit>執(zhí)行之前,父類的< clinit>方法先執(zhí)行完畢。
因此,虛擬機中第一個被執(zhí)行完畢的< 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
從輸出中看出,父類的靜態(tài)初始化塊在子類靜態(tài)變量初始化之前初始化完畢,所以輸出結果是20,不是10。
如果類或者父類中都沒有靜態(tài)變量及方法,虛擬機不會為其生成< clinit>方法。
接口與類不同的是,執(zhí)行接口的<clinit>方法不需要先執(zhí)行父接口的<clinit>方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現(xiàn)類在初始化時也一樣不會執(zhí)行接口的<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>方法在多線程環(huán)境中被正確地加鎖和同步,如果多個線程同時去初始化一個類,那么只有一個線程去執(zhí)行這個類的< clinit>方法,其他線程都需要阻塞等待,直到活動線程執(zhí)行< 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指令的目標類;
當遇到調(diào)用靜態(tài)方法或者使用靜態(tài)變量,初始化靜態(tài)變量或方法所在的類;
子類初始化過程會觸發(fā)父類初始化;
如果一個接口定義了default方法,那么直接實現(xiàn)或者間接實現(xiàn)該接口的類的初始化,會觸發(fā)該接口初始化;
使用反射API對某個類進行反射調(diào)用時,初始化這個類;
Class.forName()會觸發(fā)類的初始化
3.3、final定義的初始化
注意:對于一個使用final定義的常量,如果在編譯時就已經(jīng)確定了值,在引用時不會觸發(fā)初始化,因為在編譯的時候就已經(jīng)確定下來,就是“宏變量”。如果在編譯時無法確定,在初次使用才會導致初始化。
public?class?StaticInnerSingleton?{????/**
?????*?使用靜態(tài)內(nèi)部類實現(xiàn)單例:
?????* 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;
????}
}
看這個例子,單例模式靜態(tài)內(nèi)部類實現(xiàn)方式。我們可以看到單例實例使用final定義,但在編譯時無法確定下來,所以在第一次使用StaticInnerSingleton.getInstance()方法時,才會觸發(fā)靜態(tài)內(nèi)部類的加載,也就是延遲加載。
這里想指出,如果final定義的變量在編譯時無法確定,則在使用時還是會進行類的初始化。
3.4、ClassLoader只會對類進行加載,不會進行初始化
public?class?Tester?{????static?{
????????System.out.println("Tester類的靜態(tài)初始化塊");
????}
}
class?ClassLoaderTest?{
????public?static?void?main(String[]?args)?throws?ClassNotFoundException?{
????????ClassLoader?classLoader?=?ClassLoader.getSystemClassLoader();
????????//下面語句僅僅是加載Tester類
????????classLoader.loadClass("loader.Tester");
????????System.out.println("系統(tǒng)加載Tester類");
????????//下面語句才會初始化Tester類
????????Class.forName("loader.Tester");
????}
}
//輸出結果
//系統(tǒng)加載Tester類
//Tester類的靜態(tài)初始化塊
從輸出證明:ClassLoader只會對類進行加載,不會進行初始化;使用Class.forName()會強制導致類的初始化。
三、類加載器
類加載器負責將.class文件(不管是jar,還是本地磁盤,還是網(wǎng)絡獲取等等)加載到內(nèi)存中,并為之生成對應的java.lang.Class對象。一個類被加載到JVM中,就不會第二次加載了。
那怎么判斷是同一個類呢?
每個類在JVM中使用全限定類名(包名+類名)與類加載器聯(lián)合為唯一的ID,所以如果同一個類使用不同的類加載器,可以被加載到虛擬機,但彼此不兼容。
1、JVM類加載器分類
1.1、Bootstrap ClassLoader
Bootstrap ClassLoader為根類加載器,負責加載java的核心類庫。根加載器不是ClassLoader的子類,是有C++實現(xiàn)的。
public?class?BootstrapTest?{????public?static?void?main(String[]?args)?{
????????//獲取根類加載器所加載的全部URL數(shù)組
????????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包(以及由虛擬機參數(shù) -Xbootclasspath 指定的類)。
我們將rt.jar解壓,可以看到我們經(jīng)常使用的類庫就在這個jar包中。
1.2 、Extension ClassLoader
Extension ClassLoader為擴展類加載器,負責加載%JAVA_HOME%/jre/ext或者java.ext.dirs系統(tǒng)熟悉指定的目錄的jar包。大家可以將自己寫的工具包放到這個目錄下,可以方便自己使用。
1.3、 System ClassLoader
System ClassLoader為系統(tǒng)(應用)類加載器,負責加載加載來自java命令的-classpath選項、java.class.path系統(tǒng)屬性,或者CLASSPATH環(huán)境變量所指定的JAR包和類路徑。程序可以通過ClassLoader.getSystemClassLoader()來獲取系統(tǒng)類加載器。如果沒有特別指定,則用戶自定義的類加載器默認都以系統(tǒng)類加載器作為父加載器。
四、類加載機制
1.1、JVM主要的類加載機制。
全盤負責:當一個類加載器負責加載某個Class時,該Class所依賴和引用的其他Class也由該類加載器負責載入,除非顯示使用另一個類加載器來載入。
父類委托(雙親委派):先讓父加載器試圖加載該Class,只有在父加載器無法加載時該類加載器才會嘗試從自己的類路徑中加載該類。
緩存機制:緩存機制會將已經(jīng)加載的class緩存起來,當程序中需要使用某個Class時,類加載器先從緩存區(qū)中搜尋該Class,只有當緩存中不存在該Class時,系統(tǒng)才會讀取該類的二進制數(shù)據(jù),并將其轉(zhuǎn)換為Class對象,存入緩存中。這就是為什么更改了class后,需要重啟JVM才生效的原因。
注意:類加載器之間的父子關系并不是類繼承上的父子關系,而是實例之間的父子關系。
public?class?ClassloaderPropTest?{????public?static?void?main(String[]?args)?throws?IOException?{
????????//獲取系統(tǒng)類加載器
????????ClassLoader?systemClassLoader?=?ClassLoader.getSystemClassLoader();
????????System.out.println("系統(tǒng)類加載器:"?+?systemClassLoader);
????????/*
????????獲取系統(tǒng)類加載器的加載路徑——通常由CLASSPATH環(huán)境變量指定,如果操作系統(tǒng)沒有指定
????????CLASSPATH環(huán)境變量,則默認以當前路徑作為系統(tǒng)類加載器的加載路徑
?????????*/
????????Enumeration?eml?=?systemClassLoader.getResources("");while?(eml.hasMoreElements())?{
????????????System.out.println(eml.nextElement());
????????}//獲取系統(tǒng)類加載器的父類加載器,得到擴展類加載器
????????ClassLoader?extensionLoader?=?systemClassLoader.getParent();
????????System.out.println("系統(tǒng)類的父加載器是擴展類加載器:"?+?extensionLoader);
????????System.out.println("擴展類加載器的加載路徑:"?+?System.getProperty("java.ext.dirs"));
????????System.out.println("擴展類加載器的parant:"?+?extensionLoader.getParent());
????}
}//輸出結果//系統(tǒng)類加載器:sun.misc.Launcher$AppClassLoader@18b4aac2//file:/C:/ProjectTest/FengKuang/out/production/FengKuang///系統(tǒng)類的父加載器是擴展類加載器:sun.misc.Launcher$ExtClassLoader@1540e19d//擴展類加載器的加載路徑:C:\SorftwareInstall\java\jdk\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext//擴展類加載器的parant:null
從輸出中驗證了:系統(tǒng)類加載器的父加載器是擴展類加載器。但輸出中擴展類加載器的父加載器是null,這是因為父加載器不是java實現(xiàn)的,是C++實現(xiàn)的,所以獲取不到。但擴展類加載器的父加載器是根加載器。
1.2、類加載流程圖
圖中紅色部分,可以是我們自定義實現(xiàn)的類加載器來進行加載。
五、創(chuàng)建并使用自定義類加載器
1、自定義類加載分析
除了根類加載器,所有類加載器都是ClassLoader的子類。所以我們可以通過繼承ClassLoader來實現(xiàn)自己的類加載器。
ClassLoader類有兩個關鍵的方法:
protected Class?loadClass(String name, boolean resolve):name為類名,resove如果為true,在加載時解析該類。
protected Class?findClass(String name)?:根據(jù)指定類名來查找類。
所以,如果要實現(xiàn)自定義類,可以重寫這兩個方法來實現(xiàn)。但推薦重寫findClass方法,而不是重寫loadClass方法,因為loadClass方法內(nèi)部會調(diào)用findClass方法。
我們來看一下loadClass的源碼
protected?Class>?loadClass(String?name,?boolean?resolve)????????throws?ClassNotFoundException
????{
????????synchronized?(getClassLoadingLock(name))?{
????????????//第一步,先從緩存里查看是否已經(jīng)加載
????????????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)?{
???????????????????//第三步,如果前面都沒有找到,就會調(diào)用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加載方法流程:
判斷此類是否已經(jīng)加載;
如果父加載器不為null,則使用父加載器進行加載;反之,使用根加載器進行加載;
如果前面都沒加載成功,則使用findClass方法進行加載。
所以,為了不影響類的加載過程,我們重寫findClass方法即可簡單方便的實現(xiàn)自定義類加載。
2、實現(xiàn)自定義類加載器
基于以上分析,我們簡單重寫findClass方法進行自定義類加載。
public?class?Hello?{???public?void?test(String?str){
???????System.out.println(str);
???}
}
public?class?MyClassloader?extends?ClassLoader?{
????/**
?????*?讀取文件內(nèi)容
?????*
?????*?@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文件的全部二進制數(shù)據(jù)
????????????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文件存在,系統(tǒng)負責將該文件轉(zhuǎn)換為Class對象
????????if?(classFile.exists())?{
????????????try?{
????????????????//將Class文件的二進制數(shù)據(jù)讀入數(shù)組
????????????????byte[]?raw?=?getBytes(classFileName);
????????????????//調(diào)用ClassLoader的defineClass方法將二進制數(shù)據(jù)轉(zhuǎn)換為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的二進制數(shù)組轉(zhuǎn)換為Calss對象。
此例子很簡單,我寫了一個Hello測試類,并且編譯過后放在了當前路徑下(大家可以在findClass中加入判斷,如果沒有此文件,可以嘗試查找.java文件,并進行編譯得到.class文件;或者判斷.java文件的最后更新時間大于.class文件最后更新時間,再進行重新編譯等邏輯)。
六、總結
本篇從類加載的三大階段:加載、鏈接、初始化開始細說每個階段的過程;詳細講解了JVM常用的類加載器的區(qū)別與聯(lián)系,以及類加載機制流程,最后通過自定義的類加載器例子結束本篇。
【面試題專欄】
2019年面試官最喜歡問的28道ZooKeeper面試題
2019年全網(wǎng)最熱門的123個Java并發(fā)面試題總結
全網(wǎng)最熱門的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文件_类加载流程、类加载机制及自定义类加载器详解的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 曝长安并非唯一入股华为新平台的车企 有更
- 下一篇: unity中单位是米还是厘米_【一步数学