您好,登錄后才能下訂單哦!
本篇內(nèi)容主要講解“怎么理解java虛擬機執(zhí)行子系統(tǒng)”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“怎么理解java虛擬機執(zhí)行子系統(tǒng)”吧!
各種不同平臺的虛擬機與所有平臺都統(tǒng)一使用的程序存儲格式— 字節(jié)碼( ByteCode ) 是構(gòu)成平臺無關(guān)性的基石。
圖-Java虛擬機提供的語言無關(guān)性
虛擬機把描述類的數(shù)據(jù)從Class文件加載到內(nèi)存,并對數(shù)據(jù)進行校驗、轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機直接使用的Java類型,這就是虛擬機的類加載機制。
Java語言中類型的加載、連接以及初始化過程都是在程序運行期間完成的,這種策略雖然會使類加載時稍微增加一些性能開銷,但是會為Java應(yīng)用程序提供高度的靈活性。Java里天生就可以動態(tài)擴展語言特性就是依賴運行期間動態(tài)加載和動態(tài)連接這個特點實現(xiàn)的。比如,如果編寫一個面向接口的程序,可以等到運行時再指定其具體實現(xiàn)類;用戶可以通過Java預(yù)定義的和自定義類加載器,讓一個本地的應(yīng)用程序可以在運行時從網(wǎng)絡(luò)或其它地方加載一個二進制流作為程序代碼的一部分,這種組裝應(yīng)用程序的方式目前已廣泛應(yīng)用于Java程序之中。從最基礎(chǔ)的JSP到相對復(fù)雜的OSGI技術(shù),都使用了Java語言運行類加載的特性。
類從被加載到虛擬機內(nèi)存中開始,到卸載出內(nèi)存為止,它的整個生命周期包括:加載(Loading)、驗證(Verification)、準(zhǔn)備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)和卸載(Unloading)7個階段。其中驗證、準(zhǔn)備、解析3個部分統(tǒng)稱為連接(Linking),這7個階段的發(fā)生順序如圖:
圖-類的生命周期
加載、驗證、準(zhǔn)備、初始化和卸載這5個階段的順序是確定的,類的加載過程必須按照這種順序按部就班地開始,而解析階段則不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java語言的運行時綁定(也稱為動態(tài)綁定或晚期綁定)。
什么情況下需要開始類加載過程的第一個階段:加載?Java虛擬機規(guī)范中并沒有進行強制約束,這點可以交給虛擬機的具體實現(xiàn)來自由把握。但是對于初始化階段,虛擬機規(guī)范則是嚴(yán)格規(guī)定了有且只有5種情況必須立即對類進行“初始化”(而加載、驗證、準(zhǔn)備自然需要在此之前開始):
1)遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關(guān)鍵字實例化對象的時候、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯期把結(jié)果放入常量池的靜態(tài)字段除外)的時候,以及調(diào)用一個類的靜態(tài)方法的時候。
2)使用java.lang.reflect包的方法對類進行反射調(diào)用的時候,如果類沒有進行過初始化,則需要先觸發(fā)其初始化。
3)當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化,則需要先觸發(fā)其父類的初始化。
4)當(dāng)虛擬機啟動時,用戶需要指定一個要執(zhí)行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
5)當(dāng)使用JDK 1.7的動態(tài)語言支持時,如果一個java.lang.invoke.MethodHandle實例最后的解析結(jié)果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且這個方法句柄所對應(yīng)的類沒有進行過初始化,則需要先觸發(fā)其初始化。
“加載”是“類加載”(Class Loading)過程的一個階段。在加載階段,虛擬機需要完成以下3件事情:
1)通過一個類的全限定名來獲取定義此類的二進制字節(jié)流。
2)將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
3)在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)的訪問入口。
通過類型的完全限定名,產(chǎn)生一個代表該類型的二進制數(shù)據(jù)流的幾種常見形式:
1)從zip包中讀取,成為日后JAR、EAR、WAR格式的基礎(chǔ);
2)從網(wǎng)絡(luò)中獲取,這種場景最典型的應(yīng)用就是Applet;
3)運行時計算生成,這種場景最常用的就是動態(tài)代理技術(shù)了;
4)由其他文件生成,比如我們的JSP;
相對于類加載過程的其他階段,一個非數(shù)組類(數(shù)組類比較特殊,有虛擬機直接創(chuàng)建的)的加載階段(準(zhǔn)確地說,是加載階段中獲取類的二進制字節(jié)流的動作)是開發(fā)人員可控性最強的,因為加載階段既可以使用系統(tǒng)提供的引導(dǎo)類加載器來完成,也可以由用戶自定義的類加載器去完成,開發(fā)人員可以通過定義自己的類加載器去控制字節(jié)流的獲取方式(即重寫一個類加載器的loadClass()方法)。
加載階段完成后,虛擬機外部的二進制字節(jié)流就按照虛擬機所需的格式存儲在方法區(qū)之中,方法區(qū)中的數(shù)據(jù)存儲格式由虛擬機實現(xiàn)自行定義,虛擬機規(guī)范未規(guī)定此區(qū)域的具體數(shù)據(jù)結(jié)構(gòu)。然后在內(nèi)存中實例化一個java.lang.Class類的對象(并沒有明確規(guī)定是在Java堆中,對于HotSpot虛擬機而言,Class對象比較特殊,它雖然是對象,但是存放在方法區(qū)里面),這個對象將作為程序訪問方法區(qū)中的這些類型數(shù)據(jù)的外部接口。
驗證是鏈接階段的第一步,這一步主要的目的是確保class文件的字節(jié)流中包含的信息符合當(dāng)前虛擬機的要求,并且不會危害虛擬機自身安全。
驗證階段主要包括四個檢驗過程:文件格式驗證、元數(shù)據(jù)驗證、字節(jié)碼驗證和符號引用驗證。
1.文件格式驗證
驗證class文件格式規(guī)范,例如class文件是否已魔術(shù)0xCAFEBABE開頭 , 主、次版本號是否在當(dāng)前虛擬機處理范圍之內(nèi)等。
2.元數(shù)據(jù)驗證
這個階段是對字節(jié)碼描述的信息進行語義分析,以保證其描述的信息符合java語言規(guī)范要求。驗證點可能包括:這個類是否有父類(除了java.lang.Object之外,所有的類都應(yīng)當(dāng)有父類)、這個類是否繼承了不允許被繼承的類(被final修飾的)、如果這個類的父類是抽象類,是否實現(xiàn)了起父類或接口中要求實現(xiàn)的所有方法。
3.字節(jié)碼驗證
進行數(shù)據(jù)流和控制流分析,這個階段對類的方法體進行校驗分析,這個階段的任務(wù)是保證被校驗類的方法在運行時不會做出危害虛擬機安全的行為。如:保證訪法體中的類型轉(zhuǎn)換有效,例如可以把一個子類對象賦值給父類數(shù)據(jù)類型,這是安全的,但不能把一個父類對象賦值給子類數(shù)據(jù)類型、保證跳轉(zhuǎn)命令不會跳轉(zhuǎn)到方法體以外的字節(jié)碼命令上。
4.符號引用驗證
對類自身以外(常量池中的各種符號引用)的信息進行匹配性校驗。
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些內(nèi)存都將在方法區(qū)中進行分配。這個階段中有兩個容易產(chǎn)生混淆的知識點,首先是這時候進行內(nèi)存分配的僅包括類變量(static 修飾的變量),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在java堆中。其次是這里所說的初始值“通常情況”下是數(shù)據(jù)類型的零值,假設(shè)一個類變量定義為:
public static int value = 12;
那么變量value在準(zhǔn)備階段過后的初始值為0而不是12,因為這時候尚未開始執(zhí)行任何java方法,而把value賦值為123的putstatic指令是程序被編譯后,存放于類構(gòu)造器()方法之中,所以把value賦值為12的動作將在初始化階段才會被執(zhí)行。
上面所說的“通常情況”下初始值是零值,那相對于一些特殊的情況,如果類字段的字段屬性表中存在ConstantValue屬性,那在準(zhǔn)備階段變量value就會被初始化為ConstantValue屬性所指定的值,建設(shè)上面類變量value定義為:
public static final int value = 123;
編譯時javac將會為value生成ConstantValue屬性,在準(zhǔn)備階段虛擬機就會根據(jù)ConstantValue的設(shè)置將value設(shè)置為123。
解析階段是虛擬機常量池內(nèi)的符號引用替換為直接引用的過程。
符號引用:符號引用是一組符號來描述所引用的目標(biāo)對象,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標(biāo)即可。符號引用與虛擬機實現(xiàn)的內(nèi)存布局無關(guān),引用的目標(biāo)對象并不一定已經(jīng)加載到內(nèi)存中。
直接引用:直接引用可以是直接指向目標(biāo)對象的指針、相對偏移量或是一個能間接定位到目標(biāo)的句柄。直接引用是與虛擬機內(nèi)存布局實現(xiàn)相關(guān)的,同一個符號引用在不同虛擬機實例上翻譯出來的直接引用一般不會相同,如果有了直接引用,那引用的目標(biāo)必定已經(jīng)在內(nèi)存中存在。
類的初始化階段是類加載過程的最后一步,在準(zhǔn)備階段,類變量已賦過一次系統(tǒng)要求的初始值,而在初始化階段,則是根據(jù)程序員通過程序制定的主觀計劃去初始化類變量和其他資源,或者可以從另外一個角度來表達:初始化階段是執(zhí)行類構(gòu)造器<clinit>()方法的過程。在以下四種情況下初始化過程會被觸發(fā)執(zhí)行:
1.遇到new、getstatic、putstatic或invokestatic這4條字節(jié)碼指令時,如果類沒有進行過初始化,則需先觸發(fā)其初始化。生成這4條指令的最常見的java代碼場景是:使用new關(guān)鍵字實例化對象、讀取或設(shè)置一個類的靜態(tài)字段(被final修飾、已在編譯器把結(jié)果放入常量池的靜態(tài)字段除外)的時候,以及調(diào)用類的靜態(tài)方法的時候。
2.使用java.lang.reflect包的方法對類進行反射調(diào)用的時候
3.當(dāng)初始化一個類的時候,如果發(fā)現(xiàn)其父類還沒有進行過初始化、則需要先出發(fā)其父類的初始化
4.jvm啟動時,用戶指定一個執(zhí)行的主類(包含main方法的那個類),虛擬機會先初始化這個類
在上面準(zhǔn)備階段 public static int value = 12; 在準(zhǔn)備階段完成后 value的值為0,而在初始化階調(diào)用了類構(gòu)造器<clinit >()方法,這個階段完成后value的值為12。
虛擬機設(shè)計團隊把類加載階段中的“通過一個類的全限定名來獲取描述此類的二進制字節(jié)流”這個動作放到Java虛擬機外部去實現(xiàn),以便讓應(yīng)用程序自己決定如何去獲取所需要的類。實現(xiàn)這個動作的代碼模塊稱為“類加載器”。
對于任何一個類,都需要由加載它的類加載器和這個類來確立其在JVM中的唯一性。也就是說,兩個類來源于同一個Class文件,并且被同一個類加載器加載,這兩個類才相等。比如同一個類采用不同的類加載器去加載,在判斷對象所屬類型檢查(instanceof)時會出現(xiàn)不同。
從虛擬機的角度來說,只存在兩種不同的類加載器:一種是啟動類加載器(Bootstrap ClassLoader),該類加載器使用C++語言實現(xiàn),屬于虛擬機自身的一部分。另外一種就是所有其它的類加載器,這些類加載器是由Java語言實現(xiàn),獨立于JVM外部,并且全部繼承自抽象類java.lang.ClassLoader。
從Java開發(fā)人員的角度來看,大部分Java程序一般會使用到以下三種系統(tǒng)提供的類加載器:
1)啟動類加載器(Bootstrap ClassLoader):負責(zé)加載存放在%JAVA_HOME%\lib目錄中的,或者被-Xbootclasspath參數(shù)所指定的路徑中的,并且被java虛擬機識別的(僅按照文件名識別,如rt.jar,名字不符合的類庫,即使放在指定路徑中也不會被加載)類庫到虛擬機的內(nèi)存中,啟動類加載器無法被java程序直接引用。
2)擴展類加載器(Extension ClassLoader):由sun.misc.Launcher$ExtClassLoader實現(xiàn),負責(zé)加載%JAVA_HOME%\lib\ext目錄中的,或者被java.ext.dirs系統(tǒng)變量所指定的路徑中的所有類庫,開發(fā)者可以直接使用擴展類加載器。
3)應(yīng)用程序類加載器(Application ClassLoader):由sun.misc.Launcher$AppClassLoader實現(xiàn),負責(zé)加載用戶類路徑classpath上所指定的類庫,是類加載器ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也稱為系統(tǒng)類加載器,開發(fā)者可以直接使用應(yīng)用程序類加載器,如果程序中沒有自定義過類加載器,該加載器就是程序中默認的類加載器。
我們的應(yīng)用程序都是由這三類加載器互相配合進行加載的。
另外還有自定義類加載器。
4)自定義類加載器(必須繼承 ClassLoader)。
圖-類加載器雙親委派模型
如果一個類加載器收到了類加載的請求,它首先不會自己去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每一個層次的父加載器都是如此,因此所有的請求最終都應(yīng)該傳送到頂層的啟動類加載器中,只有當(dāng)父類加載器反饋自己無法完成這個加載請求時,子加載器才會嘗試自己去加載。雙親委派模型對于保證JAVA程序的穩(wěn)定運作很重要。例如可以嘗試去編寫一個與rt.jar類庫中已有類重名的Java類,將會發(fā)現(xiàn)可以正常編譯,但永遠無法被加載運行。
執(zhí)行引擎是Java虛擬機最核心的組成部分之一。虛擬機是一個相對于物理機的概念,這兩種機器都有代碼執(zhí)行能力,其區(qū)別是物理機的執(zhí)行引擎是直接建立在處理器、硬件、指令集和操作系統(tǒng)層面的,而虛擬機的執(zhí)行引擎則是由自己實現(xiàn)的,因此可以自行制定指令集與執(zhí)行引擎的結(jié)構(gòu)體系,并且能夠執(zhí)行那些不被硬件直接支持的指令集格式。
在Java虛擬機規(guī)范中制定了虛擬機字節(jié)碼執(zhí)行引擎的概念模型,這個概念模型成為各種虛擬機執(zhí)行引擎的統(tǒng)一外觀(Facade)。在不同的虛擬機實現(xiàn)里面,執(zhí)行引擎在執(zhí)行Java代碼的時候會有解釋執(zhí)行(通過解釋器執(zhí)行)和編譯執(zhí)行(通過即時編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇,也可能兩者兼?zhèn)洌踔量赡軙瑤讉€不同級別的編譯器執(zhí)行引擎。
方法調(diào)用并不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定被調(diào)用方法的版本(即調(diào)用哪一個方法),暫時還不涉及方法內(nèi)部的具體運行過程。我們知道,Class文件的編譯過程中并不包括傳統(tǒng)編譯中的連接步驟,一切方法調(diào)用在Class文件調(diào)用里面存儲的都只是符號引用,而不是方法在實際運行時的內(nèi)存布局入口地址(相當(dāng)于之前說的直接引用),也就是說符號引用解析成直接引用的過程。這個特性使得Java 具有強大的動態(tài)擴展能力,但也使得Java方法調(diào)用過程變得復(fù)雜起來,需要在類加載器件,甚至是運行期間才確定目標(biāo)方法的直接引用。
在類加載的解析階段,會將其中一部分符號引用直接轉(zhuǎn)化為直接引用,前提是:方法在程序真正運行之前就有一個可確定的版本,并且這個方法的調(diào)用版本在運行期是不可改變的。換句話說,調(diào)用目標(biāo)在程序代碼寫好,編譯器進行編譯時就必須確定下來。這類方法的調(diào)用稱為解析(Resolution)。
在Java語言中符合“編譯期可知,運行期不可變”這個要求的方法,主要包括:靜態(tài)方法和私有方法。前者與類型直接關(guān)聯(lián),后者在外部不可被訪問,這兩種方法各自的特點決定了他們都不可能通過繼承或別的方式重寫其它版本,因此它們適合在類加載階段進行解析。
與之相對應(yīng)的,Java 虛擬機里面提供了5條方法調(diào)用字節(jié)碼指令,分別如下:
invokestatic:調(diào)用靜態(tài)方法
invokespecial:調(diào)用<init>方法、私有方法和父類方法
invokevirtual:調(diào)用所有的虛方法
invokeinterface:調(diào)用接口方法,會在運行時在確定一個實現(xiàn)此接口的對象
invokedynamic:會在運行時動態(tài)解析出調(diào)用點限定符所引用的方法,然后再執(zhí)行該方法。
只要能被invokestatic和invokespecial調(diào)用的方法,都可以在解析階段中確定唯一的調(diào)用版本,符合這個條件的有靜態(tài)方法、私有方法、實例構(gòu)造器、父類方法4類,它們在加載的時候就會把符號引用解析為該方法的直接引用,這些方法稱為非虛方法,由于final修飾的方法不能被覆蓋,也屬于非虛方法。與之相反,其他的方法稱為虛方法。
解析調(diào)用一定是靜態(tài)的過程,在編譯期間完全確定,在類裝載的解析階段就會把涉及的符號引用全部轉(zhuǎn)換為可確定的直接引用,不會延遲到運行期再去完成。這和后邊談到的分派是完全不同的。
作為一門面向?qū)ο蟮某绦蛘Z言,Java具備面型對象的3個特征:繼承、封裝和多態(tài)。下面我們將會講解多態(tài)性特征的一些最基本的體現(xiàn),如“重寫”和“重載”在Java虛擬機中是怎么實現(xiàn)的。
靜態(tài)分派
依賴于靜態(tài)類型來定位方法執(zhí)行版本的分派動作(如重載)稱為靜態(tài)分派。虛擬機(準(zhǔn)確說是編譯器)在重載時是通過參數(shù)的靜態(tài)類型而不是實際類型作為判定依據(jù)的,并且靜態(tài)類型是編譯器可知的,因此在編譯期,Javac編譯器會根據(jù)參數(shù)的靜態(tài)類型決定使用哪個重載版本。
動態(tài)分派
運行時期依賴于實際類型來定位方法執(zhí)行的分派動作(重寫Override)屬于動態(tài)分派。
單分派與多分派
方法的接受者與方法的參數(shù)統(tǒng)稱為方法的宗量。根據(jù)分派基于多少宗量,可以將分派劃分為單分派和多分派兩種。單分派是根據(jù)一個宗量對目標(biāo)方法進行選擇,多分派則是根據(jù)多于一個宗量對目標(biāo)方法進行選擇。
在靜態(tài)分派的過程中,選擇目標(biāo)方法的依據(jù)有兩點,對象的靜態(tài)類型以及方法參數(shù)的類型和數(shù)量。因為是根據(jù)兩個宗量進行選擇,所以Java語言的靜態(tài)分派屬于多分派類型。
在動態(tài)分派的過程中,由于編譯器已經(jīng)決定了目標(biāo)方法的簽名,因此只需要找到方法的接受者就可以了。因為是根據(jù)一個宗量進行選擇,所以Java語言的動態(tài)分派屬單分派類型。
虛擬機動態(tài)分派的實現(xiàn)
由于動態(tài)分配是非常頻繁的動作,而且動態(tài)分配的方法版本選擇過程需要運行時在類的方法元數(shù)據(jù)中搜索合適的目標(biāo)方法,因此在虛擬機的實際實現(xiàn)中,基于性能的考慮,大部分實現(xiàn)都不會真正的進行如此頻繁的搜索。最常用的手段就是為類在方法區(qū)中建立一個虛方法表(Virtual Method Table , 也稱為vtable ,與此對應(yīng)的在invokeinterface執(zhí)行時也會用到接口方法表-Inteface Method Table,簡稱itable),使用虛方法表索引來代替元數(shù)據(jù)查找以提高性能。
虛方法表中存放著各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那子類的虛方法表里面的地址入口和父類相同方法的地址入口是一致的,都指向父類的實現(xiàn)入口。如果子類中重寫了這個方法,子類方法表中的地址將會替換為指向子類實現(xiàn)版本的入口。方法表一般在類加載的連接階段進行初始化,準(zhǔn)備了類的變量初始值之后,虛擬機會把該類的方法表也初始化完畢。
到此,相信大家對“怎么理解java虛擬機執(zhí)行子系統(tǒng)”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。