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

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

错误: 找不到或无法加载主类 helloworld_全面剖析虚拟机类加载机制

發(fā)布時(shí)間:2023/12/9 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 错误: 找不到或无法加载主类 helloworld_全面剖析虚拟机类加载机制 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

1.引言

java源文件經(jīng)過(guò)編譯后生成字節(jié)碼class文件,需要經(jīng)過(guò)虛擬機(jī)加載并轉(zhuǎn)換成匯編指令才能執(zhí)行,那么虛擬機(jī)是如何一步步加載這些class文件的對(duì)于java程序員是完全透明的,本文嘗試全面分析jvm類加載機(jī)制。

2.思考

開(kāi)始之前我們來(lái)簡(jiǎn)單思考一下,如果讓你來(lái)寫虛擬機(jī)類加載你覺(jué)得要怎么做?

首先,肯定有一個(gè)加載過(guò)程,虛擬機(jī)要讀取class字節(jié)碼。

其次,為了保證虛擬機(jī)的安全性,需要對(duì)輸入做校驗(yàn),只有校驗(yàn)通過(guò)了才能繼續(xù)執(zhí)行,程序設(shè)計(jì)總是這樣,才能保證系統(tǒng)安全穩(wěn)定。

再次,校驗(yàn)通過(guò)后將字節(jié)碼轉(zhuǎn)換成類對(duì)象。

最后,將類對(duì)象建立全局索引方便引用。

如果把類加載也當(dāng)成一個(gè)工程子模塊,從邏輯上看,我們上面的分析沒(méi)有什么問(wèn)題,但工程實(shí)踐經(jīng)驗(yàn)表明,實(shí)際情況肯定要復(fù)雜一些,因?yàn)殡S著深入,總有新問(wèn)題產(chǎn)生,至于復(fù)雜多少需要我們繼續(xù)深入分析。

3.類的生命周期

類從被加載到j(luò)vm內(nèi)存開(kāi)始,到卸載出內(nèi)存需要經(jīng)過(guò)7個(gè)階段:加載(Loading)、驗(yàn)證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)。

類生命周期的七個(gè)階段并非串行執(zhí)行,比如在進(jìn)行驗(yàn)證時(shí)與此同時(shí)準(zhǔn)備階段或解析階段已經(jīng)開(kāi)始了,階段是相互嵌套并行執(zhí)行的,只是按照邏輯分類可以這樣進(jìn)行區(qū)分。又比如正常情況下解析階段過(guò)后是初始化階段,但為了支持java語(yǔ)言的運(yùn)行時(shí)綁定(也成為動(dòng)態(tài)綁定或晚期綁定),在初始化階段之后才開(kāi)始解析階段。

類的主動(dòng)引用和被動(dòng)引用

什么情況下開(kāi)始類加載呢,一般下面四種情況必須立即進(jìn)行類初始化工作,如果類加載沒(méi)做自然也必須立馬做:

  • 遇到new、getstatic、putstatic、invokestatic這4條字節(jié)碼指令時(shí),如果類沒(méi)有進(jìn)行過(guò)初始化,則需要先觸發(fā)初始化。
  • 對(duì)類進(jìn)行反射調(diào)用時(shí)。
  • 當(dāng)初始化一個(gè)類時(shí),如果父類還沒(méi)初始化,則需要先初始化父類。
  • 包含main方法的主類,當(dāng)虛擬機(jī)啟動(dòng)時(shí)需要初始化該主類。
  • 以上4種情況稱為對(duì)類的主動(dòng)引用。

    除了以上4種情況,其他對(duì)類的引用被稱為被動(dòng)引用,

    case1:子類引用父類的的靜態(tài)字段,只會(huì)觸發(fā)父類的初始化而不會(huì)觸發(fā)子類初始化。

    public class SuperClass {public static int value = 123; static {System.out.println("SuperClass init!");} } public class SubClass extends SuperClass {static {System.out.println("SubClass init!");} } public class NotInitialization {public static void main(String[] args) {System.out.println(SubClass.value);} }

    上述代碼運(yùn)行之后,最后輸出的是“SuperClass init!”。

    case2:通過(guò)數(shù)組定義來(lái)引用類,不會(huì)觸發(fā)此類的初始化

    public class NotInitialization {public static void main(String[] args) {SuperClass[] superArray = new SuperClass[10];} }

    這段代碼并不會(huì)觸發(fā)SuperClass初始化,即不會(huì)輸出“SuperClass init!”。

    注:這段代碼會(huì)觸發(fā)[LSuperClass初始化,這個(gè)類代表SuperClass一維數(shù)組,相對(duì)c/c++,java對(duì)一維數(shù)組的封裝抱枕了安全性,當(dāng)數(shù)組發(fā)生越界時(shí),將拋出java.lang.ArrayIndexOutOfBoundsException。

    case3:常量在編譯階段會(huì)存入調(diào)用類的常量池中,本質(zhì)上沒(méi)有直接引用到定義常量的類,因此不會(huì)觸發(fā)定義常量的類的初始化。

    public class ConstClass {public static final String HELLOWORLD = "Hello World"; static {System.out.println("ConstClass init!");} } public class NotInitialization {public static void main(String[] args) {System.out.println(ConstClass.HELLOWORLD);} }

    對(duì)常量ConstClass.HELLOWORLD的引用實(shí)際被轉(zhuǎn)換成NotInitialization類自身常量池的引用。

    接口加載過(guò)程差異點(diǎn)

    接口和類加載過(guò)程略有不同,根本差異點(diǎn)在于:當(dāng)一個(gè)類初始化時(shí),要求其父類全部已經(jīng)初始化過(guò)了,但在一個(gè)接口初始化時(shí),并不要求其父接口全部初始化,只有在真正使用到父接口時(shí)才會(huì)初始化。

    4.類加載過(guò)程

    類加載過(guò)程包含5個(gè)階段:加載、驗(yàn)證、準(zhǔn)備、解析和初始化。下面來(lái)分析一下這5個(gè)階段JVM都做了什么。

    加載階段

    加載階段需要完成3個(gè)事:

  • 通過(guò)一個(gè)類的全限定名來(lái)獲得定義此類的二進(jìn)制字節(jié)流。
  • 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)換成方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
  • 在堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)數(shù)據(jù)的訪問(wèn)入口。
  • 虛擬機(jī)規(guī)范并沒(méi)有要求二進(jìn)制字節(jié)流要從哪里獲取,也就是說(shuō),在加載階段是開(kāi)了口子的,很開(kāi)放的,富有創(chuàng)造性的程序員在這個(gè)舞臺(tái)玩出了各種花樣,比如字節(jié)流獲取可以從:

  • 從ZIP獲取,最終形成了jar、war等格式。
  • 從網(wǎng)絡(luò)中獲取,典型場(chǎng)景是Applet。
  • 從其他文件生成,比如jsp。
  • 運(yùn)行時(shí)計(jì)算,典型場(chǎng)景就是動(dòng)態(tài)代理技術(shù):用ProxyGenerator.generateProxyClass來(lái)為特定接口生成*$Proxy代理類的二進(jìn)制字節(jié)流。
  • 還有其他方式,只有你想不到的。

    相對(duì)于類加載其他階段的透明性,加載階段是程序員可控性最強(qiáng)的階段,因?yàn)榧虞d階段可以使用系統(tǒng)提供的類加載器,也可以用戶自定義類加載器,我們?cè)谙挛倪€會(huì)詳細(xì)說(shuō)明Java的類加載器。

    驗(yàn)證階段

    驗(yàn)證階段屬于連接階段的第一步,是出于虛擬機(jī)自身安全考慮,確保二進(jìn)制字節(jié)流包含的信息符合虛擬機(jī)的要求。這也說(shuō)明了Java語(yǔ)言是相對(duì)安全的語(yǔ)言,使用純粹的Java代碼無(wú)法做到諸如訪問(wèn)數(shù)據(jù)邊界以外的數(shù)據(jù),將一個(gè)對(duì)象轉(zhuǎn)換成一個(gè)未知類型,跳轉(zhuǎn)到不存在的代碼行之類的行為。

    驗(yàn)證階段一般需要完成4個(gè)階段的校驗(yàn)過(guò)程:文件格式校驗(yàn)、元數(shù)據(jù)校驗(yàn)、字節(jié)碼校驗(yàn)和符號(hào)引用校驗(yàn)。

    文件格式校驗(yàn)

    文件格式校驗(yàn)主要是完成語(yǔ)法校驗(yàn),即檢查二進(jìn)制字節(jié)流是否符合Class文件格式規(guī)范,目標(biāo)是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)之內(nèi),格式上符合描述Java類型信息的要求。

    校驗(yàn)項(xiàng)具體包含:

  • 是否以魔數(shù)0xCAFEBABE開(kāi)頭。
  • 主次版本號(hào)是否在虛擬機(jī)處理范圍。
  • 常量池常量是否有不被支持的常量類型(即檢查常量tag標(biāo)志)。
  • CONSTANT_Utf8_info型常量中是否有不符合UTF8編碼的數(shù)據(jù)。
  • Class文件中各個(gè)部分和文件本身是否有被刪除的或被附加的其他信息。
  • ...
  • 經(jīng)過(guò)了這層驗(yàn)證,字節(jié)流才會(huì)進(jìn)入方法區(qū)內(nèi)存儲(chǔ)。我們也可以看到驗(yàn)證階段其實(shí)是和加載階段交織在一起的。

    元數(shù)據(jù)校驗(yàn)

    元數(shù)據(jù)校驗(yàn)階段是對(duì)字節(jié)碼信息進(jìn)行語(yǔ)義分析,以保證數(shù)據(jù)符合Java語(yǔ)言規(guī)范,比如是否繼承了一個(gè)不允許被繼承的父類。具體驗(yàn)證點(diǎn)包含:

  • 這個(gè)類是否有父類,因?yàn)槌薺ava.lang.Object外,所有類都有父類。
  • 這個(gè)類是否繼承了final修飾的類。
  • 如果這個(gè)類不是抽象類,那么,是否實(shí)現(xiàn)了其父類和接口之中要求實(shí)現(xiàn)的所有方法。
  • 類中的字段、方法是否與父類產(chǎn)生了矛盾:比如是否覆蓋了父類的final方法,出現(xiàn)了不符合規(guī)范的方法重載等等。
  • ...
  • 元數(shù)據(jù)階段主要是完成數(shù)據(jù)類型校驗(yàn)。

    字節(jié)碼校驗(yàn)

    字節(jié)碼驗(yàn)證階段將對(duì)類的方法體(數(shù)據(jù)流和控制流)進(jìn)行驗(yàn)證分析,是整個(gè)驗(yàn)證階段最復(fù)雜的階段。這個(gè)階段保證方法在運(yùn)行時(shí)不會(huì)出現(xiàn)危害虛擬機(jī)安全的行為。比如需要做:

  • 保證任意時(shí)刻操作數(shù)棧的數(shù)據(jù)類型與指令代碼序列都能配合工作,比如不會(huì)出現(xiàn)操作數(shù)棧放置了一個(gè)int類型的數(shù)據(jù),使用時(shí)卻按long類型來(lái)來(lái)載入本地變量表中。
  • 保證跳轉(zhuǎn)指令不會(huì)跳轉(zhuǎn)到方法體以外的字節(jié)碼指令上。
  • 保證方法體內(nèi)的類型轉(zhuǎn)換是有效的,比如允許把子類型賦值給父類數(shù)據(jù)類型,但不允許把父類對(duì)象類型賦值給子類數(shù)據(jù)類型。
  • ...
  • 但方法體內(nèi)邏輯校驗(yàn)無(wú)法做到絕對(duì)可靠,即不能指望校驗(yàn)程序準(zhǔn)確地檢查出程序能否在有限時(shí)間之內(nèi)結(jié)束運(yùn)行。

    符號(hào)引用校驗(yàn)

    符號(hào)引用校驗(yàn)可以看做是對(duì)類自身以外的信息進(jìn)行匹配性校驗(yàn),發(fā)生時(shí)機(jī)是虛擬機(jī)將符號(hào)引用轉(zhuǎn)換成直接引用時(shí)。校驗(yàn)內(nèi)容包含:

  • 符號(hào)引用中通過(guò)字符串描述的全限定名能否找到對(duì)應(yīng)的類。
  • 在指定類中是否存在符號(hào)方法的字段描述符以及簡(jiǎn)單名稱所描述的方法、字段。
  • 符號(hào)引用中類、字段和方法的訪問(wèn)性是否允許當(dāng)前類的訪問(wèn)。
  • ...
  • 符號(hào)引用驗(yàn)證的目的是保證解析動(dòng)作能正常執(zhí)行,如果沒(méi)有通過(guò)符號(hào)引用驗(yàn)證將拋出java.lang.IncompatibleClassChangeError異常的子類,比如java.lang.IllegalAccessError、java.lang.NoSuchFieldError、java.lang.NoSuchMethodError等。

    以上是驗(yàn)證階段校驗(yàn)的內(nèi)容。

    準(zhǔn)備階段

    準(zhǔn)備階段正式為類變量分配內(nèi)存并設(shè)置類變量的初始值,這些變量將在方法區(qū)中進(jìn)行分配,也就是準(zhǔn)備階段分配的是類變量,并非實(shí)例變量。

    準(zhǔn)備階段初始化類變量零值,以下是基本類型的零值:

    舉個(gè)例子,假設(shè)一個(gè)類變量定義如下:

    public static int value = 123;

    那么value在準(zhǔn)備階段初始化value = 0而非123,那什么時(shí)候會(huì)變成123呢,把value變成123的是putstatic指令是存放在類構(gòu)造器<clinit>()方法中,而類構(gòu)造器方法在初始化階段才會(huì)執(zhí)行。

    但也存在特殊情況類變量賦值不是0的情況,比如在類中定義常量,如果類字段的字段表屬性表中存在ConstantValue屬性,則在準(zhǔn)備階段就會(huì)賦值。

    public static final int value = 123;

    解析階段

    解析階段是虛擬機(jī)將常量池內(nèi)的符號(hào)引用(Symbolic References)替換為直接引用(Direct References)的過(guò)程。符號(hào)引用在符號(hào)引用驗(yàn)證中提到過(guò),它以CONSTANT_Class_info,CONSTANT_Fieldref_info、CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info等類型的常量出現(xiàn)。符號(hào)引用和直接引用的差別在于:

    • 符號(hào)引用是以一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義地定位到目標(biāo)即可。符號(hào)引用與虛擬機(jī)實(shí)現(xiàn)的內(nèi)部布局無(wú)關(guān),引用的目標(biāo)不一定已經(jīng)加載到內(nèi)存中。
    • 直接引用是可以直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。

    解析什么時(shí)候發(fā)生呢,虛擬機(jī)并未明確指定,一般虛擬機(jī)實(shí)現(xiàn)會(huì)根據(jù)需要來(lái)判斷,解析可能發(fā)生在類被加載器加載時(shí)就對(duì)常量池中的符號(hào)引用進(jìn)行解析,亦或等到一個(gè)符號(hào)引用將要使用前才去解析。

    此外,對(duì)同一個(gè)符號(hào)的多次解析請(qǐng)求是很常見(jiàn)的,為了避免重復(fù)解析,虛擬機(jī)實(shí)現(xiàn)可能會(huì)對(duì)第一次解析的結(jié)果緩存,即在運(yùn)行時(shí)常量池中記錄直接引用,并把常量標(biāo)識(shí)為已解析狀態(tài)。

    解析動(dòng)作主要針對(duì)類、接口、字段、類方法、接口方法四類符號(hào)引用。

    類或接口的解析過(guò)程

    假設(shè)當(dāng)前代碼所處的類是D,如果要把一個(gè)從未解析過(guò)的符號(hào)引用N解析為一個(gè)類或接口C的直接引用,那虛擬機(jī)完成整個(gè)解析的過(guò)程需要包含以下3個(gè)步驟:

  • 如果C不是數(shù)組類型,那虛擬機(jī)將會(huì)把代表N的全限定名傳遞給D的類加載器去加載這個(gè)類C,在加載過(guò)程中,由于元數(shù)據(jù)、字節(jié)碼驗(yàn)證的需要,可能觸發(fā)其他類的加載過(guò)程。
  • 如果C是數(shù)組類型,并且數(shù)組元素是一個(gè)對(duì)象類型(以[Ljava.lang.Integer為例),那將會(huì)按照第一點(diǎn)的規(guī)則加載數(shù)組元素類型。虛擬機(jī)會(huì)生成一個(gè)代表此數(shù)組維度和元素的數(shù)組對(duì)象。
  • 如果上述步驟沒(méi)有出現(xiàn)異常,那么C在虛擬機(jī)中已經(jīng)成為了一個(gè)有效的類或接口了。在解析前還需要檢驗(yàn)D是否具備對(duì)C的訪問(wèn)權(quán)限,如果沒(méi)有訪問(wèn)權(quán)限將拋出java.lang.IllegalAccessError。
  • 字段解析

    解析一個(gè)未被解析過(guò)的字段前,首先需要對(duì)字段表內(nèi)class_index(即字段的類索引)的CONSTANT_Class_info符號(hào)引用進(jìn)行解析,也就是或在進(jìn)行字段解析之前,需要先完成類或接口的符號(hào)解析。

    假設(shè)一個(gè)需要解析的字段所屬的類或接口為C,虛擬機(jī)規(guī)范要求按如下步驟對(duì)C后序字段進(jìn)行搜索:

  • 如果C本身就包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束。
  • 如果在C中實(shí)現(xiàn)了接口,將會(huì)按照繼承關(guān)系從上到下遞歸搜索各個(gè)接口和它的父接口,如果接口中包含簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束。
  • 如果C不是java.lang.Object,將會(huì)按照繼承關(guān)系從上往下遞歸搜索其父類,如果在父類中包含了簡(jiǎn)單名稱和字段描述符都與目標(biāo)相匹配的字段,則返回這個(gè)字段的直接引用,查找結(jié)束。
  • 否則,查找失敗,拋出java.lang.NoSuchFieldError異常。
  • 如果在父類和接口中存在同名字段會(huì)發(fā)生什么呢?如果是這樣情況,編譯器將拒絕編譯,比如下面這種情況:

    public class FieldResolution {interface Interface0 {int a = 0;} interface Interface1 extends Interface0 {int a = 1;} interface Interface2 {int a = 2;}static class Parent implements Interface1 {public static int a = 3;} static class Sub extends Parent implements Interface2 {public static int a = 4;} public static void main(String[] args) {System.out.println(Sub.a);} }


    若將Sub靜態(tài)成員變量?publc static int a = 4?注釋掉,編譯器將返回“The field Sub.A is ambiguous”。

    下面我們將類方法解析和接口方法解析,兩者是分開(kāi)的。

    類方法解析

    類解析和字段解析一樣,需要先解析出類方法表的class_index索引所屬類或接口的符號(hào)引用。如果類或接口符號(hào)引用接口成功,我們依然用C來(lái)表示類或接口,接下來(lái)將按如下步驟進(jìn)行搜索:

  • 如果發(fā)現(xiàn)C是一個(gè)接口,將拋出java.lang.IncompatibleClassChangeError異常。
  • 在類C中查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回這個(gè)方法的直接引用,查找結(jié)束。
  • 在類C的父類中遞歸查找是否有簡(jiǎn)單名稱和描述符都與目標(biāo)相匹配的方法,如果有則返回方法的直接引用,查找結(jié)束。
  • 在類C的接口列表和它們的父接口中遞歸查找是否有簡(jiǎn)單名稱和描述符和目標(biāo)相匹配的方法,如果查找到,則說(shuō)明類C是一個(gè)抽象類,將拋出java.lang.AbstractMethodError異常。
  • 若以上未查詢到,則宣告查找失敗,拋出java.lang.NoSuchMethodError。
  • 查找結(jié)束若成功返回,還需要對(duì)方法權(quán)限進(jìn)行校驗(yàn)。

    接口方法解析

    與類方法解析類似,步驟如下:

  • C是否是類,如果是則拋出java.lang.IncompatibleClassChangeError異常。
  • 在接口中查找。
  • 在接口的父接口中遞歸查找。
  • 若以上未查詢到,則宣告查找失敗,拋出java.lang.NoSuchMethodError。
  • 以上是解析階段所做的工作。

    初始化階段

    類初始化階段是類加載過(guò)程的最后一步,到了初始化階段,才真正執(zhí)行類中定義的字節(jié)碼。在準(zhǔn)備階段,變量已經(jīng)賦過(guò)一次初始值,初始化階段將執(zhí)行類構(gòu)造器<clinit>()方法的過(guò)程。

    • <clinit>()方法是由編譯器自動(dòng)收集類中的所有類變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊static{}塊中的語(yǔ)句結(jié)合產(chǎn)生的。編譯器收集的順序是由語(yǔ)句在源文件中出現(xiàn)的順序決定的,比如靜態(tài)語(yǔ)句塊只能訪問(wèn)定義在靜態(tài)語(yǔ)句塊之前的變量。
    • <clinit>()方法和類的構(gòu)造方法不同,并不需要顯示的調(diào)用父類構(gòu)造器,虛擬機(jī)保證在子類<clinit>()方法執(zhí)行之前,父類構(gòu)造器先執(zhí)行,因此,在虛擬機(jī)中第一個(gè)執(zhí)行類構(gòu)造器的類為java.lang.Object。
    • <clinit>()方法對(duì)于類或接口來(lái)說(shuō)并不是必須的。
    • 虛擬機(jī)會(huì)保證一個(gè)類的<clinit>()方法在多線程環(huán)境中被正確地加鎖和同步。

    5.類加載器(ClassLoader)

    上一節(jié)我們?cè)谝婚_(kāi)始就聊到虛擬機(jī)允許用戶自定義類加載器,這就給java語(yǔ)言帶來(lái)了很大的靈活性,類加載器可以說(shuō)是Java語(yǔ)言的一項(xiàng)創(chuàng)新,也是Java語(yǔ)言流行的重要原因之一。類加載器在類層次劃分、OSGi、熱部署、代碼加密等領(lǐng)域大放異彩,成為Java技術(shù)體系的一塊重要基石。

    類和類加載器

    類的唯一性是有類加載器決定的,比較2各類是否相等除了比較類本身還需要比較類加載器。如果一個(gè)類被2個(gè)不同的類加載器加載,那么加載的類是2個(gè)不同的類。如下所示:

    public class ClassLoaderTest {public static void main(String[] args) {ClassLoader myLoader = new ClassLoader() {@Overridepublic Class<?> loadClass(String name) throws ClassNotFoundException {try {String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";InputStream is = this.getClass().getResourceAsStream(fileName);if (is == null) {return super.loadClass(name);}byte[] b = new byte[is.available()];is.read(b);return defineClass(name, b,, 0, b.length);}catch (IOException e) {throw new ClassNotFoundException(name);}}};Object obj = myLoader.loadClass("ClassLoaderTest").newInstance();System.out.println(obj instanceof ClassLoaderTest);} }

    運(yùn)行結(jié)果:false。

    類加載器的層次結(jié)構(gòu)

    “橫看成嶺側(cè)成峰”,站在Java虛擬機(jī)角度看,只存在2種不同類型的類加載器:一種是啟動(dòng)類加載器(Bootstrap ClassLoader),這個(gè)類加載器是C++實(shí)現(xiàn)的,是虛擬機(jī)自身一部分;另外一種是其他類加載器,這種類加載器都是虛擬機(jī)外部的,由Java實(shí)現(xiàn),并且全部繼承自抽象類java.lang.ClassLoader。站在Java程序員角度看,可以分成以下3種:

  • 啟動(dòng)類加載器,上面剛提到過(guò),負(fù)載加載存放在JAVA_HOME/lib路徑中的類或者被-Xbootclasspath參數(shù)所指定路徑下的類。啟動(dòng)類加載器無(wú)法被Java程序直接引用。
  • 擴(kuò)展類加載器(Extension ClassLoader),這個(gè)加載器由sum.misc.Launcher$ExtClassLoader實(shí)現(xiàn),負(fù)責(zé)加載JAVA_HOME/lib/ext路徑下的類,或者由java.ext.dirs系統(tǒng)變量所指定下的類庫(kù)。
  • 應(yīng)用程序類加載器(Application ClassLoader),這個(gè)類加載器由sum.misc.Launcher$AppClassLoader來(lái)實(shí)現(xiàn)。由于這個(gè)類加載器可以由ClassLoader.getSystemClassLoader()獲取,因此也叫系統(tǒng)類加載器。AppClassLoader復(fù)雜加載用戶類路徑(ClassPath)上指定的類庫(kù),開(kāi)發(fā)者可以直接使用。
  • 以下是類加載器的層次結(jié)構(gòu)

    雙親委派模型

    類加載器雙親委派模型是JDK1.2引入被應(yīng)用于幾乎所有的Java程序中。但它并不是一個(gè)強(qiáng)制性的約束模型,二是Java設(shè)計(jì)者推薦的一種類加載方式。

    雙親委派有他的適用場(chǎng)景(它能夠適用于絕大多數(shù)場(chǎng)景),模型可以保證Java程序的穩(wěn)定運(yùn)行,防止重復(fù)加載和任意修改。那具體是如何做到的呢?

    雙親委派模型的工作過(guò)程如下:如果一個(gè)類加載器收到了類加載的請(qǐng)求,它首先不會(huì)自己嘗試加載這個(gè)類,而是把請(qǐng)求委派給父類加載器去完成,如上圖所示,每一層次的類加載器都是如此,因此所有的加載請(qǐng)求最終都應(yīng)該傳送到頂層的啟動(dòng)類加載器中,只有父類反饋?zhàn)约簾o(wú)法完成這個(gè)加載請(qǐng)求時(shí)(即它的搜索范圍沒(méi)有找到指定的類),字加載器才會(huì)嘗試自己加載。

    比如java.lang.Object,無(wú)論哪個(gè)類加載器需要加載這個(gè)類,最終都由Bootstrap ClassLoader加載,因此Object在程序的各個(gè)類加載器環(huán)境都是同一個(gè)類。相反,如果如果不用雙親委派模型進(jìn)行加載,用戶自定義了一個(gè)Object類并放置在類路徑下,最終可能會(huì)引發(fā)程序混亂。

    雙親委派模型很好地解決了基礎(chǔ)類的統(tǒng)一問(wèn)題,保證了虛擬機(jī)的安全性。

    非雙親委派模型

    線程上下文類加載器

    雙親委派模型適用于大部分場(chǎng)景,但也有它自身的缺陷,假設(shè)基礎(chǔ)類由Bootstrap類加載器加載,但是基礎(chǔ)類需要回調(diào)用戶的代碼,基礎(chǔ)代碼卻是由應(yīng)用類加載器加載,這個(gè)時(shí)候該怎么辦呢?

    JNDI(Java Naming and Directory Interface)服務(wù)就是上面描述的這種場(chǎng)景,JNDI是Java的標(biāo)準(zhǔn)服務(wù),它自身的代碼由Bootstrap類加載器加載,由于JNDI的目的就是對(duì)資源進(jìn)行集中管理和查找,它需要調(diào)用獨(dú)立廠商實(shí)現(xiàn)的JNDI接口提供者(SPI)的代碼,獨(dú)立廠商提供的代碼jar包放置在ClassPath下,如果使用雙親委派模型加載類的方式是搞不定的,怎么辦呢?

    為了解決這個(gè)困境,Java設(shè)計(jì)團(tuán)隊(duì)引入了線程上下文類加載器(Thread Context ClassLoader),雖然它確實(shí)不太優(yōu)雅,但解決問(wèn)題啊。這個(gè)類加載器可以通過(guò)java.lang.Thread類的setContextClassLoader()方法進(jìn)行設(shè)置,如果創(chuàng)建線程還未設(shè)置,它將從父線程中繼承一個(gè),如果在應(yīng)用程序的全局范圍內(nèi)都沒(méi)有設(shè)置過(guò),那么這個(gè)類加載器就是AppClassLoader。有了線程上下文類加載器,JNDI服務(wù)就可以加載所需要的SPI代碼,即父類加載器可以請(qǐng)求子類加載器完成類加載動(dòng)作,這其實(shí)是違反了雙親委派原則的。實(shí)際上JNDI、JDBC、JCE、JAXB、JBI等所有涉及SPI加載動(dòng)作的基本都采取的這種方式。

    總結(jié):線程上下文加載器之所以打破雙親委派模型是因?yàn)殡p親委派模型依賴的單一方向的,并不能解決父類加載器去依賴子類加載器這種逆方向需求。

    Tomcat類加載器

    實(shí)際上,不只是Driver驅(qū)動(dòng)的實(shí)現(xiàn)是這樣,只要有需要,在雙親委派機(jī)制無(wú)法滿足需求前提下,在tomcat、spring等等的容器框架也是通過(guò)一些手段繞過(guò)雙親委派機(jī)制。

    雙親委派模型要求除了頂層的啟動(dòng)類加載器之外,其余的類加載器都應(yīng)當(dāng)由自己的父類加載器加載。tomcat 為了實(shí)現(xiàn)隔離性,沒(méi)有遵守這個(gè)約定,每個(gè)webappClassLoader加載自己的目錄下的class文件,不會(huì)傳遞給父類加載器。如下圖所示

    從圖中的委派關(guān)系中可以看出:

    • CommonClassLoader能加載的類都可以被Catalina ClassLoader和SharedClassLoader使用,從而實(shí)現(xiàn)了公有類庫(kù)的共用。
    • CatalinaClassLoader和Shared ClassLoader自己能加載的類則與對(duì)方相互隔離。
    • WebAppClassLoader可以使用SharedClassLoader加載到的類,但各個(gè)WebAppClassLoader實(shí)例之間相互隔離。

    JasperLoader的加載范圍僅僅是這個(gè)JSP文件所編譯出來(lái)的那一個(gè).Class文件,它出現(xiàn)的目的就是為了被丟棄:當(dāng)Web容器檢測(cè)到JSP文件被修改時(shí),會(huì)替換掉目前的JasperLoader的實(shí)例,并通過(guò)再建立一個(gè)新的Jsp類加載器來(lái)實(shí)現(xiàn)JSP文件的HotSwap功能。

    總結(jié):tomcat之所以破壞雙親委派模型,我想主要在于雙親委派模型只看到了共享性,沒(méi)有看到隔離性需求,即共享是有條件的共享。

    OSGI類加載器

    非雙親委派模型的另一種需求來(lái)自程序動(dòng)態(tài)性追求。比如代碼熱替換(HotSwap)、模塊熱部署(Hot Deployment)。可以哪USB熱插拔技術(shù)來(lái)做比方。熱部署對(duì)生產(chǎn)系統(tǒng)來(lái)說(shuō)具有很大吸引力,不用停機(jī)就能完成部署效率啊。

    OSGI是Java模塊化標(biāo)準(zhǔn),OSGI實(shí)現(xiàn)模塊熱部署的關(guān)鍵是它自定義的類加載器,每一個(gè)模塊都有一個(gè)自己定義的類加載器,當(dāng)需要更換一個(gè)Bundle時(shí),則把Bundle連同類加載器一同替換以實(shí)現(xiàn)熱替換。

    在OSGI環(huán)境下,類加載器不再是樹(shù)型結(jié)構(gòu)的雙親委派模型,而是網(wǎng)狀結(jié)構(gòu),當(dāng)收到類加載請(qǐng)求時(shí),OSGI是按照下面順序進(jìn)行類搜索的:

  • 以“java.*”開(kāi)頭的類,委派給父類加載器加載。
  • 將委派列表名單內(nèi)的類,委派給父類加載器加載。
  • 將Import列表中的類,委派給Export這個(gè)類的Boundle的類加載器加載。
  • 查找當(dāng)前Boundle的ClassPath,使用自己的類加載器加載。
  • 查找類是否在Fragment Boundle中,如果在,則委派給Fragment Boundle類加載器加載。
  • 查找Dynamic Import列表的Boundle,委派給對(duì)應(yīng)的Boundle類加載器加載。
  • 如果以上都未查詢到,則查找失敗。

    上面的搜索順序除了1,2兩點(diǎn)和雙親委派類似,其余都是平級(jí)類加載過(guò)程。

    總結(jié):OSGI Boundle類加載器提供了類加載的另一種機(jī)制,加載器結(jié)構(gòu)不一定非得是樹(shù)型結(jié)構(gòu),也可以是網(wǎng)狀結(jié)構(gòu)。

    全文總結(jié)

    本文較全面的分析了jvm的類加載機(jī)制,分析了類加載的5個(gè)階段,包含:加載、驗(yàn)證、準(zhǔn)備、解析、初始化,最后總結(jié)了類加載器加載類的幾種模型:雙親委派模型、SPI的類加載模型、tomcat類加載模型以及OSGI類加載模型。

    The end.

    轉(zhuǎn)載請(qǐng)注明來(lái)源,否則嚴(yán)禁轉(zhuǎn)載。

    總結(jié)

    以上是生活随笔為你收集整理的错误: 找不到或无法加载主类 helloworld_全面剖析虚拟机类加载机制的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。