摘要:在加載階段,虛擬機要完成件事情通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。前面的階段中,除了加載的時候,可以由用戶指定自定義類加載器之外,別的都是由虛擬機主導(dǎo)控制。
java類加載機制
代碼編譯的結(jié)果從本地機器碼轉(zhuǎn)變?yōu)樽止?jié)碼,是存儲格式發(fā)展的一小步,確實編程語言發(fā)展的一大步
虛擬機把描述類的數(shù)據(jù)從class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的java類型,這就是虛擬機的類加載機制。
1 類的生命周期一個類從被加載到內(nèi)存到卸載出內(nèi)存,整個生命周期包括:
加載loading
驗證verification
準(zhǔn)備preparation
解析resolution
初始化initialization
使用using
卸載unloading
其中驗證、準(zhǔn)備和解析,這三步合起來又被稱為連接(liking)。
加載、驗證、準(zhǔn)備、初始化和卸載,這五個階段的順序是確定的,而解析不一定。某些情況下,解析可能在初始化之后再開始,這就是java動態(tài)綁定。
java虛擬機規(guī)范中嚴格規(guī)定了有且只有5種情況必須對類立即進行初始化:
遇到new、getstatic、putstatic或invokestatic這四個指令時,必須進行初始化。
生成這幾個指令的場景有:
使用new實例化一個對象時;
讀取或者設(shè)置一個類的靜態(tài)字段時;
調(diào)用一個類的靜態(tài)方法時。
使用reflect包的方法對類進行反射時,也觸發(fā)初始化。
初始化一個類的時候,若父類還未初始化,則首先進行父類的初始化。
包含main方法的那個類,虛擬機啟動時會首先初始化這個主類。
當(dāng)使用jdk1.7的動態(tài)語言支持時,
接口的加載和類加載的過程稍有些不同:
接口和類一樣都有初始化過程,雖然接口里面不能有static{}語句塊,但是編譯器仍然會為接口生成
java接口中的變量必須得是final靜態(tài)的,但接口里最好不要有變量。
當(dāng)一個類初始化時,必須要求父類全部都已經(jīng)初始化,但是接口在初始化時并不要求其父接口也全部初始化,只有在使用到父接口時才會初始化。
2 類加載的過程 2.1 加載加載是類加載的一個階段。在加載階段,虛擬機要完成3件事情:
通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
加載階段完成之后,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中。
2.2 驗證驗證階段是連接階段的第一步,目的是為了確保Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求,并且不會危害虛擬機自身的安全。
文件格式驗證
驗證字節(jié)流是否符合Class文件格式的規(guī)范,并且能被當(dāng)前版本的虛擬機處理。
元數(shù)據(jù)驗證
對字節(jié)碼描述的信息進行語義分析,以保證其描述信息符合java語言規(guī)范。
字節(jié)碼驗證
最復(fù)雜的一個階段,通過對數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的。
符號引用驗證
對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。目的是為了確保解析動作能正常執(zhí)行。
驗證階段是非常重要的,但不是一定必要的階段。如果所運行的代碼都已經(jīng)被反復(fù)使用和驗證過,就可以通過jvm參數(shù)來關(guān)閉大部分類驗證措施。
2.3 準(zhǔn)備準(zhǔn)備階段是給類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都將在方法區(qū)中進行分配。
此時進行內(nèi)存分配的變量僅包括類變量,而不包含實例變量,實例變量將在對象實例化時隨著對象一起分配在java堆中。
這里所說的初始值是指數(shù)據(jù)類型的零值,比如:
public static int v = 123;
那v的值在準(zhǔn)備階段是0,而不是123。
數(shù)據(jù)類型 | 零值 | 數(shù)據(jù)類型 | 零值 |
---|---|---|---|
int | 0 | boolean | false |
long | 0L | float | 0.0f |
short | (short)0 | double | 0.0d |
char | "u0000" | reference | null |
byte | (byte)0 |
如果一個變量是常量,或者final類型的,那么在準(zhǔn)備階段就被初始化為常量值,如:
public static final int v = 123;
此時v的值在準(zhǔn)備階段是123。
2.4 解析解析階段是虛擬機將常量池的符號引用替換成直接引用的過程。
符號引用:是以一組符號來描述所引用的目標(biāo),符號可以是任何形式的字面量,只要使用的時候能無歧義的定位到目標(biāo)即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)并不一定加載到內(nèi)存中。各種虛擬機的內(nèi)存布局可以各不相同,但是能接受的符號引用必須是一致的,因為符號引用的字面量形式明確定義在java虛擬機規(guī)范的Class文件格式中。
直接引用:直接引用可以是直接指向目標(biāo)的指針、相對偏移量或者是一個能間接定位到目標(biāo)的句柄。直接引用是和虛擬機實現(xiàn)的內(nèi)存相關(guān)的,如果有了直接引用,那么引用的目標(biāo)必定已經(jīng)在內(nèi)存中了。
解析主要是針對類或者接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點限定符7類符號引用進行的。
2.5 初始化類初始化時類加載過程的最后一步。前面的階段中,除了加載的時候,可以由用戶指定自定義類加載器之外,別的都是由虛擬機主導(dǎo)控制。初始化階段才真正執(zhí)行類中定義的java代碼。
在準(zhǔn)備階段變量已經(jīng)被賦過零值,而初始化階段是根據(jù)程序里面的來初始化類變量和其他資源,可以理解為執(zhí)行類構(gòu)造器的
public class Test{ static{ i=0; //這句話是給變量賦值,可以編譯通過 System.out.println(i); //這句話是要訪問i,編譯器會提示“非法向前引用”編譯不過。 } static int i = 1; }
由上一條可以得出結(jié)論,父類中定義的靜態(tài)語句塊要早于子類的變量賦值操作。
前面加載的時候有說到,接口中不能有靜態(tài)語句塊,但是可以有變量的初始化賦值操作。接口和類都會生成
虛擬機會保證一個類的
類加載階段的加載階段,即“通過一個類的全限定名來獲取描述此類的二進制字節(jié)流”這個動作放到j(luò)vm外部實現(xiàn),使得應(yīng)用程序自己可以決定如何獲取所需要的類。實現(xiàn)這個動作的代碼模塊稱為“類加載器”。
對于任意一個類來說,需要加載它的類加載器和其類本身來保證唯一性。如果同一個Class文件,被不同的類加載器加載了,那么產(chǎn)生的兩個類是不相同的。
3.1 類加載器的分類對于java虛擬機來說,只有兩種不同的類加載器:
啟動類加載器 Bootstrap ClassLoader:C++實現(xiàn)的,虛擬機的一部分。
其他類加載器:java語言實現(xiàn),獨立于jvm外部。全部繼承抽象類java.lang.ClassLoader。
從java程序員的角度來看,有三種系統(tǒng)提供的類加載器:
啟動類加載器 Bootstrap ClassLoader
負責(zé)將放在JAVA_HOEM/lib目錄里的,或者是被-Xbootclasspath參數(shù)指定的路徑中的,并且可以被虛擬機識別的類庫加載到虛擬機內(nèi)存中。
啟動類加載器無法被java程序直接引用。如果是用戶在編寫自定義類加載器的時候,需要把加載請求委派給啟動類加載器,返回null就行了。
擴展類加載器 Extension ClassLoader
負責(zé)加載JAVA_HOEM/lib/ext目錄中的,或者被java.ext.dirs系統(tǒng)變量指定的所有類庫,開發(fā)者可以直接使用擴展類加載器。
應(yīng)用程序類加載器 Application ClassLoader
這個類加載器是ClassLoader中的getSystemClassLoader()方法中的返回值,所以也稱為系統(tǒng)類加載器,負責(zé)加載用戶類路徑上指定的類庫。
開發(fā)者可以直接使用此類加載器,如果應(yīng)用程序沒有自定義自己的類加載器,一般情況下這個就是程序的默認類加載器。
開發(fā)者可以自己編寫一些自定義類加載器,用來進行特定類的加載。他們的關(guān)系是:
雙親委派模型
3.1 雙親委派模型雙親委派模型要求除了最頂層的啟動類加載器外,其余的加載器都得有自己的父類加載器。這里的類加載器的父子關(guān)系不是通過繼承來實現(xiàn),而是使用組合關(guān)系來復(fù)用復(fù)加載器的代碼。
雙親委派模型的工作過程是:
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的類加載器都是這樣。因此,所有的類加載請求最終都會傳送到最頂層的啟動類加載器,只有當(dāng)父加載器反饋自己無法加載這個加載請求的時候,子加載器才會嘗試自己去加載。
使用這個模型的好處就是java類隨著它的加載器一起具備了一種帶有優(yōu)先級的層次關(guān)系。比如java.lang.Object,無論哪個類加載器要加載這個類的時候,最終都是委派給最頂端的啟動類加載器進行加載,因此Object類在程序的各個類加載器環(huán)境中都是同一個類。如果不使用這個模型的話,由各個類加載器自己加載,就會出現(xiàn)多個Object類。
雙親委派模型的邏輯實現(xiàn)代碼很簡單:
protected synchronized Class> loadClass(String name, boolean resolve) throws ClassNotFoundException{ //首先檢查請求的類是否已經(jīng)被加載過 Class c = findLoadedClass(name); if(c==null){ if(parent!=null){ c=parent.loadClass(name, false); }else{ c=findBootstrapClassOrNull(name); } //如果父類加載器無法加載的時候,就調(diào)用本身的方法去加載 if(c==null){ c=findClass(name); } } if(resolve){ resolveClass(c); } return c; }3.2 破壞雙親委派模型
雙親委派模型并不是一個強制性的約束模型,在java世界中,大部分加載器都遵循這個模型,在java歷史上有三種比較大的被破壞情況。
第一次是jdk1.2發(fā)布的時候。由于雙親委派模型是在1.2才引入的,java.lang.ClassLoader是在1.0的時候就存在了,面對在此之前的用戶自定義類加載器的代碼,java設(shè)計者添加了一個findClass方法來作為妥協(xié)。
第二次是JNDI服務(wù)。雙親委派模型很好地解決了各個類加載器的基礎(chǔ)類統(tǒng)一問題,但是當(dāng)基礎(chǔ)類又回來調(diào)用用戶的代碼就沒辦法了。所以引入了線程上下文 類加載器(Thread Context ClassLaoder)。
第三次就是熱更新熱部署的時候。代表就是OSGi,每一個程序模塊都有一個自己的類加載器,當(dāng)需要更換一個模塊的時候,就把模塊連同其加載器一起換掉。此時的類加載器的結(jié)構(gòu)成了網(wǎng)狀結(jié)構(gòu)了。
4 寫在最后把書從后面往前面看還是挺有意思的。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/70758.html
摘要:如果需要支持類的動態(tài)加載或需要對編譯后的字節(jié)碼文件進行解密操作等,就需要與類加載器打交道了。雙親委派模型,雙親委派模型,約定類加載器的加載機制。任何之類的字節(jié)碼都無法調(diào)用方法,因為該方法只能在類加載的過程中由調(diào)用。 jvm系列 垃圾回收基礎(chǔ) JVM的編譯策略 GC的三大基礎(chǔ)算法 GC的三大高級算法 GC策略的評價指標(biāo) JVM信息查看 GC通用日志解讀 jvm的card table數(shù)據(jù)...
摘要:前面提到,對于數(shù)組類來說,它并沒有對應(yīng)的字節(jié)流,而是由虛擬機直接生成的。對于其他的類來說,虛擬機則需要借助類加載器來完成查找字節(jié)流的過程。驗證階段的目的,在于確保被加載類能夠滿足虛擬機的約束條件。 Java 虛擬機將字節(jié)流轉(zhuǎn)化為 Java 類的過程。這個過程可分為加載、鏈接以及初始化 三大步驟。 加載是指查找字節(jié)流,并且據(jù)此創(chuàng)建類的過程。加載需要借助類加載器,在 Java 虛擬機中,類...
摘要:當(dāng)前類加載器和所有父類加載器都無法加載該類時,拋出異常。加載兩份相同的對象的情況和不屬于父子類加載器關(guān)系,并且各自都加載了同一個類。類加載機制與接口當(dāng)虛擬機初始化一個類時,不會初始化該類實現(xiàn)的接口。 類加載機制 概念 類加載器把class文件中的二進制數(shù)據(jù)讀入到內(nèi)存中,存放在方法區(qū),然后在堆區(qū)創(chuàng)建一個java.lang.Class對象,用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。 1、加載: 查...
摘要:當(dāng)程序使用某個類時,如果該類還沒被初始化,加載到內(nèi)存中,則系統(tǒng)會通過加載連接初始化三個過程來對該類進行初始化。一旦一個類被加載到中之后,就不會再次載入了。它既可以從本地文件系統(tǒng)獲取二進制文件來加載類,也可以遠程主機獲取二進制文件來加載類。 當(dāng)程序使用某個類時,如果該類還沒被初始化,加載到內(nèi)存中,則系統(tǒng)會通過加載、連接、初始化三個過程來對該類進行初始化。該過程就被稱為類的初始化 類加載 ...
摘要:學(xué)習(xí)能更深入的理解這門語言,能理解語言底層的執(zhí)行過程,深入到字節(jié)碼層次。 目錄 ? 前言 程序的運行 1.JVM類加載機制 ①一般在什么情況下會去加載一個類?也就是說,什么時候.class字節(jié)碼文件中加載這個類到JVM內(nèi)存里來? ②驗證、準(zhǔn)備、初始化 ③初始化 2.類加載器和雙親委派機制 ...
摘要:以上文中的類的加載過程為例,它的加載器為系統(tǒng)類加載器。自定義加載器編寫自定義加載器并不困難,只要繼承抽象類并覆蓋方法就行了。源碼來自參考資料類加載機制與類加載器架構(gòu)深入探討類加載器 序 我是在關(guān)于Java的面試題里了解到類加載器的,在這之前從未想過Java里類是如何被加載、解析的,一直以為只要Import就好了。事實上Java類加載器是一塊非常重要的內(nèi)容,可以用在類層次劃分、OSGi、...
閱讀 2982·2021-10-14 09:42
閱讀 3679·2021-10-11 10:59
閱讀 3012·2019-08-30 11:25
閱讀 3141·2019-08-29 16:25
閱讀 3282·2019-08-26 17:40
閱讀 1326·2019-08-26 13:30
閱讀 1218·2019-08-26 11:46
閱讀 1391·2019-08-23 15:22