欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Java 類的加載機(jī)制

 更新時(shí)間:2020年08月25日 10:32:48   作者:早起的小蟲子  
這篇文章主要介紹了Java 類的加載機(jī)制,幫助大家更好的理解和學(xué)習(xí)Java,感興趣的朋友可以了解下

一、類的加載機(jī)制

  虛擬機(jī)把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型,這就是虛擬機(jī)的類加載機(jī)制。

  類的加載指的是將類的.class文件中的二進(jìn)制數(shù)據(jù)讀入到內(nèi)存中,將其放在運(yùn)行時(shí)數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個(gè)java.lang.Class對(duì)象,用來(lái)封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。類的加載的最終產(chǎn)品是位于堆區(qū)中的Class對(duì)象,Class對(duì)象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問(wèn)方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口。

  類加載器并不需要等到某個(gè)類被“首次主動(dòng)使用”時(shí)再加載它,JVM規(guī)范允許類加載器在預(yù)料某個(gè)類將要被使用時(shí)就預(yù)先加載它,如果在預(yù)先加載的過(guò)程中遇到了.class文件缺失或存在錯(cuò)誤,類加載器必須在程序首次主動(dòng)使用該類時(shí)才報(bào)告錯(cuò)誤(LinkageError錯(cuò)誤)如果這個(gè)類一直沒(méi)有被程序主動(dòng)使用,那么類加載器就不會(huì)報(bào)告錯(cuò)誤

加載.class文件的方式

– 從本地系統(tǒng)中直接加載
– 通過(guò)網(wǎng)絡(luò)下載.class文件,這種場(chǎng)景最典型的應(yīng)用就是Applet
– 從zip,jar等歸檔文件中加載.class文件
– 從專有數(shù)據(jù)庫(kù)中提取.class文件
– 將Java源文件動(dòng)態(tài)編譯為.class文件

 二、類的加載時(shí)機(jī)

  類從被加載到虛擬機(jī)內(nèi)存中開始,直到卸載出內(nèi)存為止,它的整個(gè)生命周期包括了:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載這7個(gè)階段。其中,驗(yàn)證、準(zhǔn)備和解析這三個(gè)部分統(tǒng)稱為連接(linking)。

  其中,加載、驗(yàn)證、準(zhǔn)備、初始化和卸載這五個(gè)階段的順序是確定的,類的加載過(guò)程必須按照這種順序按部就班的“開始”(僅僅指的是開始,而非執(zhí)行或者結(jié)束,因?yàn)檫@些階段通常都是互相交叉的混合進(jìn)行,通常會(huì)在一個(gè)階段執(zhí)行的過(guò)程中調(diào)用或者激活另一個(gè)階段),而解析階段則不一定(它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語(yǔ)言的運(yùn)行時(shí)綁定(也稱為動(dòng)態(tài)綁定或晚期綁定)。

三、類的加載過(guò)程

  接下來(lái)詳細(xì)講解一下Java虛擬機(jī)中類加載的全過(guò)程,也就是加載、驗(yàn)證、準(zhǔn)備、解析和初始化這五個(gè)階段所執(zhí)行的具體動(dòng)作。

  3.1 加載

  “加載”(Loading)階段是“類加載”(Class Loading)過(guò)程的第一個(gè)階段,在此階段,虛擬機(jī)需要完成以下三件事情:

1、 通過(guò)一個(gè)類的全限定名來(lái)獲取定義此類的二進(jìn)制字節(jié)流。

2、 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。

3、 在Java堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象,作為方法區(qū)這些數(shù)據(jù)的訪問(wèn)入口。

  上面的第一步獲取二進(jìn)制字節(jié)流,并沒(méi)有限定只能從編譯好的.class文件中獲取,也可以是zip包,jar,war,網(wǎng)絡(luò)流(Applet),運(yùn)行時(shí)計(jì)算生成(如動(dòng)態(tài)代理,通過(guò)反射在運(yùn)行時(shí)動(dòng)態(tài)生成代理類),其他文件(如jsp,因jsp最終會(huì)編譯成class),數(shù)據(jù)庫(kù)(用的場(chǎng)景較少)。

  對(duì)于數(shù)組類的加載,和普通類的加載有所不同。數(shù)組類本身不通過(guò)類加載器加載,而是由虛擬機(jī)直接完成。但是數(shù)組類的元素類型(指數(shù)組類去除維度之后的類型,如String[] 數(shù)組的元素類型就是 String)是靠類加載器加載的。

  加載階段完成之后,虛擬機(jī)就會(huì)把外部的二進(jìn)制字節(jié)流(不論從何處獲取的)按照一定的數(shù)據(jù)格式存儲(chǔ)在運(yùn)行時(shí)數(shù)據(jù)區(qū)中的方法區(qū)。然后在內(nèi)存中實(shí)例化一個(gè)java.lang.Class對(duì)象(Class這個(gè)對(duì)象比較特殊,它存放在方法區(qū)中而不是堆中),這個(gè)對(duì)象將作為程序訪問(wèn)方法區(qū)中的這些數(shù)據(jù)的外部接口。

加載階段即可以使用系統(tǒng)提供的類加載器在完成,也可以由用戶自定義的類加載器來(lái)完成。加載階段與連接階段的部分內(nèi)容(如一部分字節(jié)碼文件格式驗(yàn)證動(dòng)作)是交叉進(jìn)行的,加載階段尚未完成,連接階段可能已經(jīng)開始,但這些夾在加載階段之中進(jìn)行的動(dòng)作,仍然屬于連接階段的內(nèi)容,這兩個(gè)階段的開始時(shí)間仍然保持著固定的先后順序。

  3.2 驗(yàn)證

驗(yàn)證是連接階段的第一步,這一階段的目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)危害虛擬機(jī)自身的安全。

Java語(yǔ)言本身是相對(duì)安全的語(yǔ)言,使用Java編碼是無(wú)法做到如訪問(wèn)數(shù)組邊界以外的數(shù)據(jù)、將一個(gè)對(duì)象轉(zhuǎn)型為它并未實(shí)現(xiàn)的類型等,如果這樣做了,編譯器將拒絕編譯。但是,Class文件并不一定是由Java源碼編譯而來(lái),可以使用任何途徑,包括用十六進(jìn)制編輯器(如UltraEdit)直接編寫。如果直接編寫了有害的“代碼”(字節(jié)流),而虛擬機(jī)在加載該Class時(shí)不進(jìn)行檢查的話,就有可能危害到虛擬機(jī)或程序的安全。

不同的虛擬機(jī),對(duì)類驗(yàn)證的實(shí)現(xiàn)可能有所不同,但大致都會(huì)完成下面四個(gè)階段的驗(yàn)證:文件格式驗(yàn)證、元數(shù)據(jù)驗(yàn)證、字節(jié)碼驗(yàn)證和符號(hào)引用驗(yàn)證。

1、文件格式驗(yàn)證,是要驗(yàn)證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機(jī)處理。如驗(yàn)證魔數(shù)是否0xCAFEBABE;主、次版本號(hào)是否正在當(dāng)前虛擬機(jī)處理范圍之內(nèi);常量池的常量中是否有不被支持的常量類型……該驗(yàn)證階段的主要目的是保證輸入的字節(jié)流能正確地解析并存儲(chǔ)于方法區(qū)中,經(jīng)過(guò)這個(gè)階段的驗(yàn)證后,字節(jié)流才會(huì)進(jìn)入內(nèi)存的方法區(qū)中存儲(chǔ),所以后面的三個(gè)驗(yàn)證階段都是基于方法區(qū)的存儲(chǔ)結(jié)構(gòu)進(jìn)行的。

2、元數(shù)據(jù)驗(yàn)證,是對(duì)字節(jié)碼描述的信息進(jìn)行語(yǔ)義分析,以保證其描述的信息符合Java語(yǔ)言規(guī)范的要求??赡馨ǖ尿?yàn)證如:這個(gè)類是否有父類;這個(gè)類的父類是否繼承了不允許被繼承的類;如果這個(gè)類不是抽象類,是否實(shí)現(xiàn)了其父類或接口中要求實(shí)現(xiàn)的所有方法……

3、字節(jié)碼驗(yàn)證,主要工作是進(jìn)行數(shù)據(jù)流和控制流分析,保證被校驗(yàn)類的方法在運(yùn)行時(shí)不會(huì)做出危害虛擬機(jī)安全的行為。如果一個(gè)類方法體的字節(jié)碼沒(méi)有通過(guò)字節(jié)碼驗(yàn)證,那肯定是有問(wèn)題的;但如果一個(gè)方法體通過(guò)了字節(jié)碼驗(yàn)證,也不能說(shuō)明其一定就是安全的。

4、符號(hào)引用驗(yàn)證,發(fā)生在虛擬機(jī)將符號(hào)引用轉(zhuǎn)化為直接引用的時(shí)候,這個(gè)轉(zhuǎn)化動(dòng)作將在“解析階段”中發(fā)生。驗(yàn)證符號(hào)引用中通過(guò)字符串描述的權(quán)限定名是否能找到對(duì)應(yīng)的類;在指定類中是否存在符合方法字段的描述符及簡(jiǎn)單名稱所描述的方法和字段;符號(hào)引用中的類、字段和方法的訪問(wèn)性(private、protected、public、default)是否可被當(dāng)前類訪問(wèn)

驗(yàn)證階段對(duì)于虛擬機(jī)的類加載機(jī)制來(lái)說(shuō),不一定是必要的階段。如果所運(yùn)行的全部代碼確認(rèn)是安全的,可以使用-Xverify:none參數(shù)來(lái)關(guān)閉大部分的類驗(yàn)證措施,以縮短虛擬機(jī)類加載時(shí)間。

  3.3 準(zhǔn)備

  準(zhǔn)備階段是類變量分配內(nèi)存并設(shè)置初始值的階段。這里的類變量指的是被static修飾的變量,而不包括實(shí)例變量。類變量被分配到方法區(qū)中,而實(shí)例變量存放在堆中。

  這里的初始值指的是數(shù)據(jù)類型的默認(rèn)值,而不是代碼中所賦的值。例如

  publicstaticintvalue = 1 ;

  在準(zhǔn)備階段之后,value值為0,而不是1。賦值為1的動(dòng)作發(fā)生在初始化階段。

  但是,也要特殊情況,如果變量被static 和 final同時(shí)修飾,則準(zhǔn)備階段直接賦值為指定值。如

  public finallystaticintvalue = 1 ;

  在準(zhǔn)備階段之后,value的值即為1.

  各數(shù)據(jù)類型的初始默認(rèn)值如下:

  3.4 解析

  解析階段是將常量池中的符號(hào)引用轉(zhuǎn)換為直接引用的過(guò)程。那什么是符號(hào)引用和直接引用呢?

  符號(hào)引用是用一組符號(hào)來(lái)描述所引用的目標(biāo),符號(hào)可以是任何形式的字面量,只要使用時(shí)能無(wú)歧義的定位到目標(biāo)即可(前面JVM的模型中,也提到了符號(hào)引用,它存在于常量池中,包括類和接口的全限定名、字段的名稱和描述符、方法的名稱和描述符)。看概念可能比較抽象,可以理解為它就是一個(gè)代號(hào),就像你有一個(gè)大名,同時(shí)也有一個(gè)小名,但是不管怎么叫指代的都是你本人。

  直接引用可以是直接指向目標(biāo)的指針、相對(duì)偏移量或是一個(gè)能間接定位到目標(biāo)的句柄。

  解析動(dòng)作主要針對(duì)類或接口、字段、類方法、接口方法、方法屬性、方法句柄、調(diào)用點(diǎn)限定符7類符號(hào)引用。此處分別介紹一下前四種的解析過(guò)程。

  1、類或接口的解析

  如果類C不是數(shù)組類型,那么虛擬機(jī)會(huì)把類C直接傳給類加載器。如果類C是數(shù)組類型并且元素類型是對(duì)象(如String[]),那么先用類加載器加載元素類型(String類型),再由虛擬機(jī)創(chuàng)建代表此數(shù)組維度和元素的數(shù)組對(duì)象。判斷調(diào)用類是否有權(quán)限訪問(wèn)被加載類,如果不允許的話,就拋出IllegalAccessError異常。

  2、字段的解析

  首先解析字段所屬的類或接口的符號(hào)引用。如果類中有字段的符號(hào)引用(字段的名稱和描述符)和目標(biāo)字段相匹配,則返回這個(gè)字段的直接引用。如果沒(méi)有,則自下而上查找其實(shí)現(xiàn)的接口和父接口,若匹配到,則返回這個(gè)字段的直接引用。如果還沒(méi)有,就自下而上查找其繼承的父類,若匹配到,則返回這個(gè)字段的直接引用。否則,查找失敗,拋出NoSuchFieldError異常。最后如果查找成功的話,會(huì)判斷字段訪問(wèn)權(quán)限,如果該字段不允許訪問(wèn),則拋出 IllegalAccessError異常。

  3、類方法解析

  類方法解析第一步同字段解析一樣,也需要先解析方法所屬的類或接口的符號(hào)引用。類方法和接口方法符號(hào)引用的常量類型是分開的。如果,在類方法中解析出來(lái)的是一個(gè)接口,則會(huì)拋出 IncompatibleClassChangeError 異常。如果在類中有方法的符號(hào)引用(方法的名稱和描述符)和目標(biāo)方法相匹配,則返回這個(gè)方法的直接引用,查找結(jié)束。否則,在類的父類中遞歸查找,若找到則返回,查找結(jié)束。否則,查找它實(shí)現(xiàn)的接口和父接口,如果找到,說(shuō)明此類是一個(gè)抽象類,拋出 AbstractMethodError異常。若都找不到,就拋出NoSuchMethodError 異常。最后,如果查找成功,會(huì)判斷此方法是否有訪問(wèn)權(quán)限,若沒(méi)有,則拋出 IllegalAccessError異常。

  4、接口方法的解析

  首先解析方法所屬的類或接口的符號(hào)引用,和類方法解析同理,如果發(fā)現(xiàn)解析出來(lái)是一個(gè)類方法,則會(huì)拋出 IncompatibleClassChangeError 異常。如果所屬接口中匹配到目標(biāo)方法,則返回此方法的直接引用。否則,在父接口中查找,若找到,則返回。否則,查找失敗,拋出 NoSuchMethodError 異常。由于接口的方法都是public的,所以不存在訪問(wèn)權(quán)限的問(wèn)題。

  3.5 初始化

  這是類加載的最后一步,到這才真正開始執(zhí)行Java代碼。在準(zhǔn)備階段,已經(jīng)為類變量分配內(nèi)存,并賦值了默認(rèn)值。在初始階段,則可以根據(jù)需要來(lái)賦值了。可以說(shuō),初始化階段是執(zhí)行類構(gòu)造器 < clinit > 方法的過(guò)程。

  首先說(shuō)下類構(gòu)造器 < clinit > 方法和實(shí)例構(gòu)造器 < init > 方法有什么區(qū)別。< clinit > 方法是在類加載的初始化階段執(zhí)行,是對(duì)靜態(tài)變量、靜態(tài)代碼塊進(jìn)行的初始化。而< init > 方法是new一個(gè)對(duì)象,即調(diào)用類的 constructor方法時(shí)才會(huì)執(zhí)行,是對(duì)非靜態(tài)變量進(jìn)行的初始化。

  類構(gòu)造器方法有如下特點(diǎn):

  保證父類的 < clinit > 方法執(zhí)行完畢,再執(zhí)行子類的 < clinit > 方法。由于父類的 < clinit > 方法先執(zhí)行,所以父類的靜態(tài)代碼塊也優(yōu)于子類執(zhí)行。如果類中沒(méi)有靜態(tài)代碼塊,也沒(méi)有為變量賦值,則可以不生成 < clinit > 方法。執(zhí)行接口的 < clinit > 方法時(shí),不需要先執(zhí)行父接口的 < clinit > 方法。只有父接口中定義的變量使用時(shí),父接口才會(huì)初始化。另外,接口的實(shí)現(xiàn)類在初始化時(shí)也不執(zhí)行接口的 < clinit > 方法。虛擬機(jī)會(huì)保證在多線程環(huán)境下 < clinit > 方法能被正確的加鎖、同步。如果有多個(gè)線程同時(shí)請(qǐng)求加載一個(gè)類,那么只會(huì)有一個(gè)線程去執(zhí)行這個(gè)類的 < clinit > 方法,其他線程都會(huì)阻塞,直到方法執(zhí)行完畢。同時(shí),其他線程也不會(huì)再去執(zhí)行 < clinit > 方法了。這就保證了同一個(gè)類加載器下,一個(gè)類只會(huì)初始化一次。(這也是為什么說(shuō)餓漢式單例模式是線程安全的,因?yàn)轭愔粫?huì)加載一次。)類的初始化時(shí)機(jī):只有對(duì)類主動(dòng)使用的時(shí)候才會(huì)觸發(fā)初始化,主動(dòng)使用的場(chǎng)景如下:

  使用new關(guān)鍵詞創(chuàng)建對(duì)象時(shí),訪問(wèn)某個(gè)類的靜態(tài)變量或給靜態(tài)變量賦值時(shí),調(diào)用類的靜態(tài)方法時(shí)。反射調(diào)用時(shí),會(huì)觸發(fā)類的初始化(如Class.forName())初始化一個(gè)類的時(shí)候,如其父類未初始化,則會(huì)先觸發(fā)父類的初始化。虛擬機(jī)啟動(dòng)時(shí),會(huì)先初始化主類(即包含main方法的類)。另外,也有些場(chǎng)景并不會(huì)觸發(fā)類的初始化:

  通過(guò)子類調(diào)用父類的靜態(tài)變量,只會(huì)觸發(fā)父類的初始化,而不會(huì)觸發(fā)子類的初始化(因?yàn)?,?duì)于靜態(tài)變量,只有直接定義這個(gè)變量的類才會(huì)初始化)。通過(guò)數(shù)組來(lái)創(chuàng)建對(duì)象不會(huì)觸發(fā)此類的初始化。(如定義一個(gè)自定義的Person[] 數(shù)組,不會(huì)觸發(fā)Person類的初始化)通過(guò)調(diào)用靜態(tài)常量(即static final修飾的變量),并不會(huì)觸發(fā)此類的初始化。因?yàn)?,在編譯階段,就已經(jīng)把final修飾的變量放到常量池中了,本質(zhì)上并沒(méi)有直接引用到定義常量的類,因此不會(huì)觸發(fā)類的初始化。

四、題目分析

  上面很詳細(xì)的介紹了類的加載時(shí)機(jī)和類的加載過(guò)程,通過(guò)上面的理論來(lái)分析本文開門見(jiàn)上的題目

class SingleTon {
  private static SingleTon singleTon = new SingleTon();
  public static int count1;
  public static int count2 = 0;
 
  private SingleTon() {
    count1++;
    count2++;
  }
 
  public static SingleTon getInstance() {
    return singleTon;
  }
}
 
public class Test {
  public static void main(String[] args) {
    SingleTon singleTon = SingleTon.getInstance();
    System.out.println("count1=" + singleTon.count1);
    System.out.println("count2=" + singleTon.count2);
  }
}

分析:

  1、SingleTon singleTon = SingleTon.getInstance();調(diào)用了類的SingleTon調(diào)用了類的靜態(tài)方法,觸發(fā)類的初始化
  2、類加載的時(shí)候在準(zhǔn)備過(guò)程中為類的靜態(tài)變量分配內(nèi)存并初始化默認(rèn)值 singleton=null count1=0,count2=0
  3、類初始化化,為類的靜態(tài)變量賦值和執(zhí)行靜態(tài)代碼快。singleton賦值為new SingleTon()調(diào)用類的構(gòu)造方法
  4、調(diào)用類的構(gòu)造方法后count=1;count2=1
  5、繼續(xù)為count1與count2賦值,此時(shí)count1沒(méi)有賦值操作,所有count1為1,但是count2執(zhí)行賦值操作就變?yōu)?

參考:

《深入理解Java虛擬機(jī):JVM高級(jí)特性與最佳實(shí)踐》

以上就是詳解Java 類的加載機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Java 類的加載的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

最新評(píng)論