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