从JVM看类的加载过程与对象实例化过程
一. 類的加載過程
1. 類的加載過程大致是個什么過程?
我們編寫產(chǎn)生.java文件,這些.java文件經(jīng)過Java編譯器編譯成拓展名為.class的文件,.class文件中保存著Java代碼經(jīng)轉(zhuǎn)換后的虛擬機(jī)指令,我們需要將類的.class文件通過類加載器加載成為二進(jìn)制流進(jìn)入內(nèi)存,即JVM運(yùn)行時數(shù)據(jù)區(qū)中的方法區(qū)中成為一種數(shù)據(jù)結(jié)構(gòu)。然后,在運(yùn)行時數(shù)據(jù)區(qū)中(沒有明確規(guī)定是在堆中)生成一個java.lang.Class對象,這個對象用來封裝加載到方法區(qū)的類的數(shù)據(jù)結(jié)構(gòu),便于向Java開發(fā)者提供用來訪問方法區(qū)中類的數(shù)據(jù)結(jié)構(gòu)的接口。
再來先放一張刻在Java程序員DNA里的圖便于直觀的討論
2. 類的具體加載過程
2.1 加載(Loading)
- 通過一個類的全限定名來獲取定義此類的二進(jìn)制字節(jié)流(并沒有指明要從一個Class文件中獲取,可以從其他渠道,譬如:網(wǎng)絡(luò)、動態(tài)生成、數(shù)據(jù)庫等)
- 將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)
- 在內(nèi)存中(對于HotSpot虛擬就而言就是方法區(qū))生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口
2.2 驗(yàn)證(Verification)
驗(yàn)證是連接階段的第一步,這一過程的目的是為了確保.Class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機(jī)的要求,并且不會危害虛擬機(jī)自身的安全。 驗(yàn)證階段大致會完成4個階段的檢驗(yàn)動作:
- 文件格式驗(yàn)證:驗(yàn)證字節(jié)流是否符合.Class文件格式的規(guī)范(例如,是否以魔術(shù)0xCAFEBABE開頭(cafe babe咖啡寶貝,就問你夠不夠騷氣)、主次版本號是否在當(dāng)前虛擬機(jī)的處理范圍之內(nèi)、常量池中的常量是否有不被支持的類型)
- 元數(shù)據(jù)驗(yàn)證:對字節(jié)碼描述的信息進(jìn)行語義分析,以保證其描述的信息符合Java語言規(guī)范的要求(例如:這個類是否有父類,除了java.lang.Object之外)
- 字節(jié)碼驗(yàn)證:通過數(shù)據(jù)流和控制流分析,確定程序語義是合法的、符合邏輯的
- 符號引用驗(yàn)證:確保解析動作能正確執(zhí)行
一個正兒八經(jīng)的.class文件字節(jié)碼格式要求如下,以下數(shù)據(jù)項(xiàng)的數(shù)量和順序都是嚴(yán)格限制死的
u2,u4,u8分別對應(yīng)2字節(jié),4字節(jié),8字節(jié)無符號數(shù)
以_info結(jié)尾表明該數(shù)據(jù)是表形式
| u4 | magic | 1 |
| u2 | minor_version | 1 |
| u2 | major_version | 1 |
| u2 | constant_pool_count | 1 |
| cp_info | constant_pool | constant_pool_count-1 |
| u2 | access_flags | 1 |
| u2 | this_class | 1 |
| u2 | super_class | 1 |
| u2 | interfaces_count | 1 |
| u2 | interfaces | interface_count |
| u2 | fields_count | 1 |
| field_info | fields | fields_count |
| u2 | methods_count | 1 |
| method_info | methods | method_count |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attributes_count |
2.3 準(zhǔn)備(Preparation)
準(zhǔn)備階段是正式為類變量(static 成員變量)分配內(nèi)存并設(shè)置類變量初始值(零值)的階段,是連接階段的第二步,這些變量所使用的內(nèi)存都將在方法區(qū)中進(jìn)行分配。這時候進(jìn)行內(nèi)存分配的僅包括類變量,而不包括實(shí)例變量,實(shí)例變量將會在對象實(shí)例化時隨著對象一起分配在堆中。其次,這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值。
假設(shè)一個類變量的定義為: public static int value = 123;
那么,變量value在準(zhǔn)備階段過后的值為0而不是123
因?yàn)檫@時候尚未開始執(zhí)行任何Java方法,而把value賦值為123的 public static指令是在程序編譯后,存放于類構(gòu)造器<clinit>()方法之中的,所以把value賦值為123的動作將在初始化階段才會執(zhí)行。
注意:如果是static final類型的常量屬性,它會被直接賦予所給定的初始值
假設(shè)一個常量定義為:public static final int constValue = 123;
那么,變量constValue準(zhǔn)備階段后的值就為123
原因是在加載生成.class字節(jié)碼文件時,在attributes屬性表中有一個ConstantValue屬性類型,如果類變量是static final修飾的常量,就會生成一個ConstantValue屬性來進(jìn)行初始化。
2.4 解析(Resolution)
解析階段連接的最后一部,也是虛擬機(jī)將常量池內(nèi)的符號引用替換為直接引用的過程。解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調(diào)用點(diǎn)限定符7類符號引用進(jìn)行。
2.5 初始化(Initialization)
類初始化階段是類加載過程的最后一步。在前面的類加載過程中,除了在加載階段用戶應(yīng)用程序可以通過自定義類加載器參與之外,其余動作完全由虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才真正開始執(zhí)行類中定義的Java程序代碼(字節(jié)碼)。
在準(zhǔn)備階段,變量已經(jīng)賦過一次系統(tǒng)要求的初始值(零值);而在初始化階段,則根據(jù)開發(fā)者通過程序制定的主觀計(jì)劃去初始化類變量和其他資源,或者更直接地說:初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。<clinit>()方法是由編譯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊static{}中的語句合并產(chǎn)生的,編譯器收集的順序是由語句在源文件中出現(xiàn)的順序所決定的,靜態(tài)語句塊只能訪問到定義在靜態(tài)語句塊之前的變量。
(PS:類構(gòu)造器使用<clinit>()進(jìn)行初始化,而實(shí)例構(gòu)造器使用的是<init>()方法
注意:如果該類的直接父類還沒有被初始化,那么先初始化其直接父類,也就是先執(zhí)行父類的<clinit>方法
3.類的加載器
在虛擬機(jī)提供了三種類加載器:
- 啟動(Bootstrap)類加載器
- 擴(kuò)展(Extension)類加載器
- 系統(tǒng)(System)類加載器(也稱應(yīng)用類加載器)
3.1 啟動類加載器(Bootstrap)
當(dāng)我們每天打開自己的電腦時,第一個運(yùn)行的就是我們的引導(dǎo)程序,即Bootstrap,所以看這加載器的名字就知道,是一個非常底層的類加載器,它主要加載的是JVM自身需要的類,這個類是由C++實(shí)現(xiàn)的,是虛擬機(jī)自身的一部分。
它負(fù)責(zé)將 <JAVA_HOME>/lib路徑下的核心類庫或-Xbootclasspath參數(shù)指定的路徑下的jar包加載到內(nèi)存中,注意必由于虛擬機(jī)是按照文件名識別加載jar包的,如rt.jar,如果文件名不被虛擬機(jī)識別,即使把jar包丟到lib目錄下也是沒有作用的,出于安全考慮,Bootstrap啟動類加載器只加載包名為java、javax、sun等開頭的類。
啟動類加載器是無法被Java程序直接引用的。
3.2 擴(kuò)展類加載器(Extension)
該加載器由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn),它負(fù)責(zé)加載 JDK\jre\lib\ext目錄中,或者由 java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫(如javax.開頭的類),開發(fā)者可以直接使用擴(kuò)展類加載器。
3.3 系統(tǒng)類加載器(System/Application)
該類加載器由 sun.misc.Launcher$AppClassLoader來實(shí)現(xiàn),它負(fù)責(zé)加載系統(tǒng)類路徑j(luò)ava -classpath或-D java.class.path指定路徑下的類庫,也就是我們經(jīng)常用到的classpath路徑,開發(fā)者可以直接使用該類加載器,如果應(yīng)用程序中沒有自定義過自己的類加載器,一般情況下這個就是程序中默認(rèn)的類加載器。
注意:
應(yīng)用程序都是由這三種類加載器互相配合進(jìn)行加載的,如果有必要,我們還可以加入自定義的類加載器。
Java虛擬機(jī)對class文件采用的是按需加載的方式,也就是說當(dāng)需要使用該類時才會將它的class文件加載到內(nèi)存生成class對象,而且加載某個類的class文件時,Java虛擬機(jī)采用的是雙親委派模式即把請求交由父類處理,它一種任務(wù)委派模式,下面我們進(jìn)一步了解它。
3.4 雙親委派模型
雙親委派模型的工作流程是:如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把請求委托給父加載器去完成,依次向上,因此,所有的類加載請求最終都應(yīng)該被傳遞到頂層的啟動類加載器中,只有當(dāng)父加載器在它的搜索范圍中沒有找到所需的類時,即無法完成該加載,子加載器才會嘗試自己去加載該類。
注意不同層的類加載器不是繼承關(guān)系,而是通過組合實(shí)現(xiàn)類加載器調(diào)用上一級加載功能。
雙親委派機(jī)制:
1、當(dāng) AppClassLoader加載一個class時,它首先不會自己去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。
2、當(dāng) ExtClassLoader加載一個class時,它首先也不會自己去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。
3、如果 BootStrapClassLoader加載失敗(例如在 $JAVA_HOME/jre/lib里未查找到該class),會使用 ExtClassLoader來嘗試加載;
4、若ExtClassLoader也加載失敗,則會使用 AppClassLoader來加載,如果 AppClassLoader也加載失敗,則會報(bào)出異常 ClassNotFoundException。
實(shí)現(xiàn)代碼:
protected sychronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{//檢查請求的類是否已經(jīng)被加載Class c = findLoadedClass(name);if(c == null){try{if(parent != null){c = parent.loadClass(name, false);}else{c = findBootstrapClassOrNull(name);}}catch(ClassNotFoundException e){//父類無法完成加載}if(c == null){//父類無法加載,調(diào)用本身的findClass方法來進(jìn)行類加載c = findClass(name);}}if(resolve){resolveClass(c);}return c; }二. 創(chuàng)建對象的過程
當(dāng)一個對象被創(chuàng)建時,虛擬機(jī)就會為其在堆中分配內(nèi)存來存放對象自己的實(shí)例變量及其從父類繼承過來的實(shí)例變量(即使這些從超類繼承過來的實(shí)例變量有可能被隱藏但也會被分配空間)。在為這些實(shí)例變量分配內(nèi)存的同時,這些實(shí)例變量也會被賦予默認(rèn)值(零值)。在內(nèi)存分配完成之后,Java虛擬機(jī)就會開始對新創(chuàng)建的對象按照程序猿的意志進(jìn)行初始化。在Java對象初始化過程中,主要涉及三種執(zhí)行對象初始化的結(jié)構(gòu),分別是 實(shí)例變量初始化、實(shí)例代碼塊初始化 以及 構(gòu)造函數(shù)初始化。(從父類到子類)
一個對象創(chuàng)建過程的偽代碼,instance = new Singleton();
由于JVM的重排序,又可能步驟2和步驟3的過程互換。
-
Java要求在實(shí)例化類之前,必須先實(shí)例化其超類,以保證所創(chuàng)建實(shí)例的完整性。事實(shí)上,這一點(diǎn)是在構(gòu)造函數(shù)中保證的:Java強(qiáng)制要求Object對象(Object是Java的頂層對象,沒有超類)之外的所有對象構(gòu)造函數(shù)的第一條語句必須是超類構(gòu)造函數(shù)的調(diào)用語句或者是類中定義的其他的構(gòu)造函數(shù),如果我們既沒有調(diào)用其他的構(gòu)造函數(shù),也沒有顯式調(diào)用超類的構(gòu)造函數(shù),那么編譯器會為我們自動生成一個對超類構(gòu)造函數(shù)的調(diào)用
-
我們在定義(聲明)實(shí)例變量的同時,還可以直接對實(shí)例變量進(jìn)行賦值或者使用普通代碼塊對其進(jìn)行賦值。如果我們以這兩種方式為實(shí)例變量進(jìn)行初始化,那么它們將在構(gòu)造函數(shù)執(zhí)行之前完成這些初始化操作。
實(shí)際上,如果我們對實(shí)例變量直接賦值或者使用實(shí)例代碼塊賦值,那么編譯器會將其中的代碼放到類的構(gòu)造函數(shù)中去,并且這些代碼會被放在對超類構(gòu)造函數(shù)的調(diào)用語句之后,構(gòu)造函數(shù)本身的代碼之前。所以從最終的初始化順序來看,這些初始化操作在構(gòu)造器之前完成。 -
類構(gòu)造器<clinit>()與實(shí)例構(gòu)造器<init>()不同,它不需要程序員進(jìn)行顯式調(diào)用,虛擬機(jī)會保證在子類類構(gòu)造器<clinit>()執(zhí)行之前,父類的類構(gòu)造<clinit>()執(zhí)行完畢。由于父類的構(gòu)造器<clinit>()先執(zhí)行,也就意味著父類中定義的靜態(tài)代碼塊/靜態(tài)變量的初始化要優(yōu)先于子類的靜態(tài)代碼塊/靜態(tài)變量的初始化執(zhí)行。
特別地,類構(gòu)造器<clinit>()對于類或者接口來說并不是必需的,如果一個類中沒有靜態(tài)代碼塊,也沒有對類變量的賦值操作,那么編譯器可以不為這個類生產(chǎn)類構(gòu)造器<clinit>()。此外,在同一個類加載器下,一個類只會被初始化一次,但是一個類可以任意地實(shí)例化對象。也就是說,在一個類的生命周期中,類構(gòu)造器<clinit>()最多會被虛擬機(jī)調(diào)用一次,而實(shí)例構(gòu)造器<init>()則會被虛擬機(jī)調(diào)用多次,只要程序員還在創(chuàng)建對象。 -
一個實(shí)例變量在對象初始化的過程中會被賦值幾次?
我們知道,JVM在為一個對象在堆中分配完內(nèi)存之后,會給每一個實(shí)例變量賦予默認(rèn)零值,這個時候?qū)嵗兞勘坏谝淮钨x值,這個賦值過程是沒有辦法避免的。如果我們在聲明實(shí)例變量x的同時對其進(jìn)行了賦值操作,那么這個時候,這個實(shí)例變量就被第二次賦值了。如果我們在實(shí)例代碼塊中,又對變量x做了初始化操作,那么這個時候,這個實(shí)例變量就被第三次賦值了。如果我們在構(gòu)造函數(shù)中,也對變量x做了初始化操作,那么這個時候,變量x就被第四次賦值。也就是說,在Java的對象初始化過程中,一個實(shí)例變量最多可以被初始化4次。當(dāng)然了,一般不會這樣做,除非特殊情況。
總的來說就是:父類的類構(gòu)造器<clinit>() ->子類的類構(gòu)造器<clinit>() -> 父類的成員變量和實(shí)例代碼塊 -> 父類的構(gòu)造函數(shù) -> 子類的成員變量和實(shí)例代碼塊 -> 子類的構(gòu)造函數(shù)
有關(guān)Java靜態(tài)域、塊,非靜態(tài)域、塊、構(gòu)造函數(shù)的初始化順序?
對于靜態(tài)變量、靜態(tài)初始化塊、變量、初始化塊、構(gòu)造器,它們的初始化順序以此是(靜態(tài)變量、靜態(tài)初始化塊)>(實(shí)例變量、初始化塊)> 構(gòu)造器。靜態(tài)代碼塊是在類加載時自動執(zhí)行的,非靜態(tài)代碼塊是在創(chuàng)建對象時自動執(zhí)行的代碼,不創(chuàng)建對象不執(zhí)行該類的非靜態(tài)代碼塊。
靜態(tài)代碼塊 與 靜態(tài)方法?
一般情況下,如果有些代碼必須在項(xiàng)目啟動的時候就執(zhí)行的時候,需要使用靜態(tài)代碼塊,這種代碼是主動執(zhí)行的;需要在項(xiàng)目啟動的時候就初始化,在不創(chuàng)建對象的情況下,其他程序來調(diào)用的時候,需要使用靜態(tài)方法,這種代碼是被動執(zhí)行的。
兩者的區(qū)別就是:靜態(tài)代碼塊是自動執(zhí)行的;靜態(tài)方法是被調(diào)用的時候才執(zhí)行的。
作用:靜態(tài)代碼塊可用來初始化一些項(xiàng)目最常用的變量或?qū)ο?#xff1b;靜態(tài)方法可用作不創(chuàng)建對象也可能需要執(zhí)行的代碼;
總結(jié)
以上是生活随笔為你收集整理的从JVM看类的加载过程与对象实例化过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 红黑树,看不懂你找我
- 下一篇: JMM内存模型如何为并发保驾护航