天天看點

深入Java虛拟機——類型裝載、連接配接(轉)

Java虛拟機通過裝載、連接配接和初始化一個Java類型,使該類型可以被正在運作的Java程式所使用。其中,裝載就是把二進制形式的Java類型讀入Java虛拟機中;而連接配接就是把這種已經讀入虛拟機的二進制形式的類型資料合并到虛拟機的運作時狀态中去。連接配接階段分為三個子步驟——驗證、準備和解析。“驗證”步驟確定了Java類型資料格式正确并且适于Java虛拟機使用而“準備”步驟則負責為該類型配置設定它所需的記憶體,比如為它的類變量配置設定記憶體。“解析”步驟則負責把常量池中的符号引用轉換為直接引用。虛拟機的實作可以推遲解析這一步,它可以在當運作中的程式真正使用某個符号引用時再去解析它(把該符号引用轉換為直接引用)。當驗證準備和(可選的)解析步驟都完成了時,該類型就已經為初始化做好了準備。在初始化期間,都将給類變量賦以适當的初始值。整個過程如圖7-1所示

就像在圖7-1中看到的那樣,裝載、連接配接和初始化這三個階段必須按順序進行。唯一的例外就是連接配接階段的第三步——解析,它可以在初始化之後再進行。

在類和接口被裝載和連接配接的時機上,Java虛拟機規範給實作提供了一定的靈活性。但是它嚴格定義了初始化的時機。所有的Java虛拟機實作必須在每個類或接口首次主動使用時初始化。下面這六種情形符合主動使用的要求。

1.      當建立某個類的新執行個體時(或者通過在位元組碼中執行new指令;或者通過不明确的建立、反射、克隆或者反序列化)。

2.      當嗲用某個類的靜态方法時(即在位元組碼中執行invokestatic指令時)。

3.      當使用某個類或接口的靜态字段,或者對該字段指派時(即在位元組碼中,執行getstatic或putstatic指令時),用final修飾的靜态字段除外,它被初始化為一個編譯時的常量表達式。

4.      當調用Java API的某些反射方法時,比如類Class中的方法或者java.lang.reflect包中的類的方法

5.      當初始化某個類的子類時(某個類初始化時,要求它的超類已經被初始化了)。

6.      當虛拟機啟動時某個被表明為啟動類的類(即含有main()方法的那個類)

除上述這六種情形外,所有其他使用Java類型的方式都是被動使用,他們都不會導緻Java類型的初始化。

在上面我們曾提到,任何一個類的初始化都要求它的超類在此之前已經初始化了。以此類推,該規則就意味着某個類的所有祖先類必須在該類之前被初始化。然而,對于接口來說,這條規則并不适用。隻有在某個接口所聲明的非常量字段被使用時,該接口才會被初始化,而不會因為實作這個接口的子接口或類要初始化而被初始化。因而,任何一個類的初始化都要求它的所有祖先類(而不是祖先接口)預先被初始化。而一個接口的初始化,并不要求它的祖先接口預先被初始化。

“在首次主動使用時初始化”這個規則直接影響着裝載、連接配接和初始化類的機制。在首次主動使用時,其類型必須被初始化。然而,在類型能被初始化之前,它必須已經被連接配接了,而在它能被連接配接之前,它必須已經被加載了。Java虛拟機的實作可以根據需要在更早的時候裝載以及連接配接類型,沒有必要一直要等到該類型的首次主動使用采取裝載和連接配接它。無論如何,如果一個類型在它的首次主動使用之前還沒有被裝載和連接配接的話,那它必須在此時被裝載和連接配接,這樣它才能被初始化。

7.1.1 裝載

裝載階段由三個基本動作組成,要裝載一個類型,Java虛拟機必須:

1.      通過該類型的完全限定名,産生一個代表該類型的二進制資料流。

2.      解析這個二進制資料流為方法區内的内部資料結構

3.      建立一個表示該類型的java.lang.Class類的執行個體

這個二進制資料流可能遵守java class檔案格式,但是也可能遵守其他的格式。就像前一章提到的那樣,所有的Java虛拟機實作必須能識别Java class檔案格式,但是個别的實作也可以識别其他的二進制格式。

Java虛拟機規範并沒有說Java類型的二進制資料應該怎樣産生。下面是一些可能的産生“類型的二進制資料”的方式:

1.      從本地檔案系統裝載一個Java class檔案。

2.      通過網絡系在一個Java class檔案。

3.      從一個ZIP、Jar、CAB或者其他某種歸檔檔案中提取Javaclass檔案。

4.      從一個專有資料庫中提取Java class檔案。

5.      把一個Java 源檔案動态編譯為class檔案格式。

6.      動态為某個類型計算其class檔案資料

7.      使用上述任何方法,但是使用不同于Java class檔案的其他二進制檔案格式。

有了類型的二進制資料之後,Java虛拟機必須對這些資料進行足夠的處理,然後它才能建立java.lang.Class的執行個體對象。虛拟機必須把這些二進制資料解析為與實作相關的内部資料結構。裝載步驟的最終産品就是這個Class類的執行個體對象,它成為Java程式與内部資料之間的接口。要通路關于該類型的資訊(它們是存儲在内部資料結構中的),程式就要調用該類型對應的Class執行個體對象的方法。這樣一個過程,就是把一個類型的二進制資料解析為方法區的内部資料結構、并在堆上建立一個Class對象的過程,這被稱為“建立”類型。

就像在前一章曾提到的,Java類型要麼由啟動類裝載器裝載,要麼通過使用者自定義的類裝載器裝載。啟動類裝載器是虛拟機實作的一部分,它以與實作無關的方式裝載類型(包括JavaAPI的類和接口),使用者自定義的類裝載器是類java.lang.ClassLoader的子類執行個體,它以定制的方式裝入類。

類裝載器(啟動型或者使用者自定義的)并不需要一直等到某個類型“首次主動使用”時再去裝入它。Java虛拟機規範允許類裝載器緩存Java類型的二進制表現形式,在預料某個類型将要被使用時就裝載它,或者把這些類型裝載到一些相關的分組裡面。如果一個類裝載器在預先裝載時遇到問題,無論如何,它應該在該類型被首次主動使用時報告該問題(通過抛出一個LinkageError異常的子類)。換句話說,如果一個類裝載器在預先裝載時遇到缺失或者錯誤的class檔案,它必須等到程式首次主動使用該類時才報告錯誤。如果這個類一直沒有被程式主動使用,那麼該類裝載器将不會報告錯誤。

7.1.2         驗證

當類型被裝載後,就準備連接配接了。連接配接過程的第一步是驗證——确認類型符合Java語言的語義,并且它不會危及虛拟機的完整性。

在驗證上,不同的虛拟機實作擁有一些靈活性。虛拟機實作的設計者可以決定如何以及何時驗證類型。Java虛拟機規範列出了虛拟機可以抛出的異常以及在何種條件下必須抛出它們。不管Java虛拟機可能遇到了什麼樣的麻煩,都應該有一個異常或者錯誤可以抛出。規範表明了在每種情形下應該抛出何種異常或者錯誤。某些情況下,規範明确地說明何時這種異常或者錯誤應該被抛出,但是通常沒有嚴格地規定應該如何或者在何時檢查錯誤條件。

不管怎樣,在大多數Java虛拟機實作中特定類型的檢查一般都在特定的時間發生。比如,在裝載過程中,虛拟機必須解析代表類型的二進制資料流,并且構造内部資料結構。在這個時候,必須做一些特定的檢查,以保證解析二進制資料的初始工作不會導緻虛拟機奔潰。在這個解析期間,虛拟機大多會檢查二進制資料以確定資料全部是預期的格式。Javaclass檔案格式的解析器可能檢查魔數,確定每一個部分都在正确的位置,擁有正确的長度,驗證檔案不是太長或者太短,等等。雖然這些檢查在裝載期間完成,實在正式的連接配接驗證階段之前進行,但它們仍然在邏輯上屬于驗證階段。檢查被裝載的類型是否有任何問題的整個過程都屬于驗證。

另外一個很可能在裝載時進行的檢查是,確定除了Object之外的每一個類都有一個超類。在裝載時檢查的原因是當虛拟機裝載一個類時,它必須確定該類的所有超類都已經被裝載了。對于給定的類,得到其超類名字的唯一方法就是觀察類的二進制資料。因為Java虛拟機無論如何都要在裝載的時候檢查每個類的超類資料,是以在裝載階段做這個檢查是順理成章的。

在大部分虛拟機實作中,還有一種檢查往往發生在正式的驗證階段之後,那就是符号引用的驗證。在前面的章節中描述過,動态連接配接的過程包括通過儲存在常量池中的符号引用朝着被引用的類、接口、字段以及方法,把符号引用替換成直接引用。當虛拟機搜尋一個被符号引用的元素時,它必須首先确認該元素存在。如果虛拟機發現元素存在,它必須進一步檢查引用類型有通路元素的權限。這些對存在性和通路權限的檢查邏輯上是驗證的一部分,屬于連接配接的第一階段,但是往往在解析的時候發生,那是連接配接的第三階段。解析自身也可能延遲到符号引用第一次被程式所使用時,是以這些檢查甚至有可能在初始化之後才進行。

那麼在正式的驗證階段做哪些檢查呢?任何在此之前還沒有進行的檢查以及在此之後不會被檢查的項目都包含在内。在正式的驗證階段需要完成的候選檢查在下面列出。首先列出確定各個類之間二進制相容的檢查:

1.      檢查final的類不能擁有子類。

2.      檢查final的方法不能被覆寫

3.      確定在類型和超類型之間沒有不相容的方法聲明(比如兩個方法擁有同樣的名字,參數在數量順序、類型上都相同,但是傳回類型不同)。

請注意,當這些檢查需要檢視其它類型的時候,它隻需要檢視超類型。超類需要在子類初始化前被初始化,是以這些類應該已經被裝載了。當實作了父接口的類被初始化的時候,不需要初始化父接口。然而,當實作了父接口的子類(或者是擴充了父接口的子接口)被裝載時,父接口也必須被裝載。(它們不會被初始化,隻是被裝載了,可能被某些虛拟機實作可選地連接配接了。)裝載一個類的時候,它所有的超類都會被裝載。在驗證期間,這個類和它所有的超類型都需要確定互相之間仍然二進制相容。

檢查所有的常量池入口互相之間一緻(比如,一個CONSTANT_String_info入口的string_index項目必須是一個CONSTANT_Utf8_info入口的索引)

檢查常量池中的所有的特殊字元串(類名、字段名和方法名、字段描述符和方法描述符)是否符合格式。

檢查位元組碼的完整性

上面列出的最複雜的任務就是位元組碼驗證。所有的Java虛拟機都必須設法為它們執行的每個方法檢驗位元組碼的完整性。比如,不能因為一個超出了方法末尾的跳轉指令就導緻虛拟機實作崩潰。它們必須在位元組碼驗證的時候檢查出這樣的跳轉指令是非法的,進而抛出一個錯誤。

虛拟機的實作沒有強求在正式的連接配接驗證階段進行位元組碼驗證。所有的Java虛拟機都必須設法為它們執行的每個方法驗證位元組碼的完整性。比如,不能因為一個超出了方法末尾的跳轉指令就導緻虛拟機實作崩潰。它們必須在位元組碼驗證的時候檢查出這樣的跳轉指令是非法的,進而抛出一個錯誤。

虛拟機的實作沒有強求在正式的連接配接驗證階段進行位元組碼驗證。比如,實作可以自由地選擇在執行每條語句的時候單獨進行驗證。然而,Java虛拟機指令集設計的一個目标就是使得位元組碼流可以通過一次性使用一個資料流分析器進行驗證。在連接配接過程中一次性驗證位元組碼流,而非在程式執行的時候動态驗證,使得Java程式的運作速度得到很大的提高。

當通過一個資料流分析器進行位元組碼驗證的時候,虛拟機可能不得不為了確定符合Java語言的語義而裝載其他的類。比如,設想一個類包含了一個方法,其中把一個Java.lang的執行個體的引用指派給了一個java.lang.Number類型的字段。在這個情況下,虛拟機将在位元組碼驗證的時候裝載類Float。確定這是一個Number類的子類。它也不得不裝載Number來確定它沒有被聲明為final。虛拟機此時不需要初始化Float,隻需要裝載它。Float會在首次主動使用時被初始化。

7.1.3         準備

随着Java虛拟機裝載了一個類,并執行了一些它選擇進行的驗證之後,類就可以進入準備階段了。在準備階段,Java虛拟機為類變量配置設定記憶體,設定預設初始值。但在到達初始化階段之前,類變量都沒有被初始化為真正的初始值。(在準備階段是不會執行Java代碼的。)在準備階段,虛拟機把給類标量新配置設定的記憶體根據類型設定預設值。不同類型的預設值在表7-1中列出。

雖然在表7-1中出現了boolean類型,Java虛拟機不太支援boolean。在内部boolean常常被實作為一個int,會被預設地置為0(就是boolean取false值)。是以boolean類變量,就算他們在内部是被作為int實作的,也總是被初始化成false。

在準備階段,Java虛拟機實作可能也為一些資料結構配置設定記憶體,目的是提高運作程式的性能。這種資料結構的例子如方法表,它包含指向類中每一個方法(包括從超類繼承的方法)的指針。方法表可以使得繼承的方法執行時不需要搜尋超類

7.1.4         解析

類型經過了連接配接的前兩個階段——驗證和準備——之後,它就可以進入第三個(也就是最後一個)連接配接階段了——解析。解析過程就是在類型的常量池中尋找類、接口、字段和方法的符号引用,把這些符号引用替換成直接引用的過程。

<a href="http://lqzit.iteye.com/blog/1758320">http://lqzit.iteye.com/blog/1758320</a>