Java虛擬機通過(guò)裝載、連接和初始化一個(gè)Java類(lèi)型,使該類(lèi)型可以被正在運行的Java程序所使用。
1. 裝載
裝載階段包括三個(gè)基本動(dòng)作:
a) 通過(guò)該類(lèi)型的完全限定名,產(chǎn)生一個(gè)代表該類(lèi)型的二進(jìn)制數據流。
b) 解析這個(gè)二進(jìn)制數據流為方法區的內部數據結構(方法區)
c) 創(chuàng )建一個(gè)表示該類(lèi)型的java.lang.Class類(lèi)的實(shí)例(堆上)
二進(jìn)制數據的產(chǎn)生的可能是很多的,比如:從本地系統裝載一個(gè)class文件、網(wǎng)絡(luò )下載一個(gè)class文件等等,有了這些二進(jìn)制數據之后,java虛擬機就會(huì )對這些數據進(jìn)行足夠的處理,然后它才能創(chuàng )建java.lang.Class的實(shí)例對象。
裝載步驟的最終產(chǎn)品就是這個(gè)Class類(lèi)的實(shí)例對象,它成為Java程序與內部數據結構之間的接口。要訪(fǎng)問(wèn)關(guān)于該類(lèi)型的信息,程序就要調用該類(lèi)型對應的Class對應的方法。
這樣一個(gè)過(guò)程就是把一個(gè)二進(jìn)制數據解析為方法區中的內部數據結構、并在堆上建立一個(gè)Class對象的過(guò)程,稱(chēng)為“創(chuàng )建”類(lèi)型。
2. 連接
在類(lèi)型被裝載以后,就準備連接了。連接過(guò)程的第一步就是:驗證
2.1 驗證
在裝載過(guò)程中,虛擬機必須解析代表類(lèi)型的二進(jìn)制數據流,并且構造內部數據結構,這時(shí)候就必須做一些特定的檢查,以保證解析二進(jìn)制數據的初始工作不會(huì )導致虛擬機崩潰。比如確保每一個(gè)部分在正確的位置,擁有正確的長(cháng)度,驗證文件不是太長(cháng)或者太短,等等,雖然這些檢查在裝載期間完成,是在正式連接驗證之前完成,但是它們在邏輯上仍然屬于驗證階段。
在正式的驗證階段可能需要完成一下檢查:
l 檢查final的類(lèi)不能擁有子類(lèi)
l 檢查final的方法不能被覆蓋
l 確保在類(lèi)型和超類(lèi)型之間沒(méi)有不兼容的方法聲明
l 檢查所有常量池入口相互之間一致
l 檢查常量池中所有的特殊字符串
l 檢查字節碼完整性
上面列出的最復雜的任務(wù)就是:字節碼完驗證。所有的Java需以及都必須設法為它們執行的每一個(gè)方法檢查字節碼的完整性。
2.2 準備
在準備階段,Java虛擬機為類(lèi)變量分配內存,設置默認初始值。但在到達初始化之前,類(lèi)變量都沒(méi)有被初始化為真正的初始值(準備階段不執行Java代碼)。
2.3 解析
解析過(guò)程就是在類(lèi)型的常量池中尋找類(lèi)、接口、字段和方法的符號引用,把這些符號引用替換成直接引用的過(guò)程。
3. 初始化
初始化就是為類(lèi)變量賦予正確的初始值。
3.1 初始化的時(shí)機-------在首次主動(dòng)使用前初始化
在類(lèi)和接口被裝載和連接上,Java虛擬機規范提供了一定的靈活性。但是它嚴格地定義了初始化的時(shí)機。所有的Java虛擬機實(shí)現必須在每個(gè)類(lèi)或接口首次主動(dòng)使用時(shí)初始化。下面6種情況符合主動(dòng)使用的要求:
l 當創(chuàng )建某個(gè)類(lèi)的新實(shí)例時(shí)(new;或者不明確的創(chuàng )建。反射??寺』蛘叻葱蛄谢?/font>
l 調用某個(gè)類(lèi)的靜態(tài)方法
l 使用某個(gè)類(lèi)或接口的靜態(tài)字段,或者對該字段賦值(final修飾的除外,它被初始化為一個(gè)編譯時(shí)的常量表達式)
l 調用Java API中的某些反射方法
l 當初始化某個(gè)類(lèi)的子類(lèi)時(shí)(要求超類(lèi)也已經(jīng)初始化)
l 當虛擬機啟動(dòng)時(shí)某個(gè)被表明為啟動(dòng)類(lèi)的類(lèi)(main()方法那個(gè)類(lèi))
除了上述6種情況以外,所有其他使用Java類(lèi)型的方式都是被動(dòng)使用。它們都不回導致Java類(lèi)型的初始化。
對于第三點(diǎn),使用一個(gè)非常量的靜態(tài)字段只有當類(lèi)或者接口的確使用了這個(gè)字段時(shí)才是主動(dòng)使用。
比如,類(lèi)中聲明的字段可能會(huì )被子類(lèi)引用;接口中聲明的字段可能會(huì )被子接口或者實(shí)現了這個(gè)接口的類(lèi)引用,對于子類(lèi)、子接口和實(shí)現了接口的類(lèi)來(lái)說(shuō),這就是被動(dòng)使用------使用它們并不會(huì )觸發(fā)它們的初始化。只有當字段的確是被類(lèi)或者接口聲明的時(shí)候才是主動(dòng)使用。
當然,超類(lèi)和子類(lèi)的規則對于接口并不適用,一個(gè)接口的初始化不要求它的父接口預先被初始化。只有在某個(gè)接口所聲明的非常量字段被使用時(shí),該接口才會(huì )被初始化,而不是因為這個(gè)接口的子接口或類(lèi)要初始化而被初始化。
無(wú)論如何,如果一個(gè)類(lèi)型在它的首次使用之前還沒(méi)有被裝載和連接的話(huà),那它必須不在此時(shí)被裝載和連接,這樣它才能被初始化。裝載、連接可以在更早的時(shí)候進(jìn)行。
3.2 初始化初探
為類(lèi)變量賦予正確的初始值是通過(guò):類(lèi)變量初始化語(yǔ)句或者靜態(tài)初始化語(yǔ)句給出的。
一個(gè)類(lèi)變量初始化語(yǔ)句是變量聲明后面的等號和表達式:
class Example1a {
// "= 3 * (int) (Math.random() * 5.0)" is the class variable
// initializer
static int size = 3 * (int) (Math.random() * 5.0);
}
靜態(tài)初始化語(yǔ)句是一個(gè)以static關(guān)鍵字開(kāi)頭的程序快:
class Example1b {
static int size;
// This is the static initializer
static {
size = 3 * (int) (Math.random() * 5.0);
}
}
所有的類(lèi)變量初始化語(yǔ)句和類(lèi)型的靜態(tài)初始化器都被Java編譯器收集在一起,放到一個(gè)特殊的方法中,稱(chēng)為 類(lèi)/接口 初始化方法<clinit>。
初始化接口并不需要初始化它的父接口,因為初始化接口只需要一步:如果接口存在初始化方法,就執行那個(gè)方法。
3.3 <clinit>方法不是必須的
并非所有的類(lèi)都需要在它們的class文件中擁有一個(gè)<clinit>方法。只有那些的確需要執行Java代碼來(lái)賦予類(lèi)變量正確初始值的類(lèi)才會(huì )有類(lèi)初始化方法。以下情況就不會(huì )有<clinit>方法:
l 類(lèi)沒(méi)有聲明任何類(lèi)變量,也沒(méi)有靜態(tài)初始化語(yǔ)句。
l 聲明了類(lèi)變量,但是沒(méi)有明確使用類(lèi)變量初始化語(yǔ)句或者靜態(tài)初始化語(yǔ)句
l 僅包含靜態(tài)final變量的類(lèi)變量初始化語(yǔ)句,而且這些類(lèi)變量初始化語(yǔ)句采用編譯時(shí)常量表達式
下面的代碼就不會(huì )產(chǎn)生<clinit>方法:
class Example1d {
static final int angle = 35;
static final int length = angle * 2;
}
兩個(gè)常量賦予的初始化,這些表達式是編譯時(shí)常量。Example1d類(lèi)被Java虛擬機裝載的時(shí),angle和length字段并非類(lèi)變量,它們是常量,不是保存在方法區中,Java虛擬機在使用它們的任何類(lèi)的常量池或者字節碼流中直接存放的是它們表示的常量的int值。因此不需要<clinit>方法
3.4 接口中的<clinit>方法
所有的接口中聲明的隱式公開(kāi)(public)、靜態(tài)(static)和最終(final)字段都必須在字段初始化語(yǔ)句中初始化。如果接口包含任何不能在編譯時(shí)被解析成為一個(gè)常量的字段初始化語(yǔ)句,接口就會(huì )有一個(gè)<clinit>方法。 這兩個(gè)隱式公開(kāi)的字段只有mustard被<clinit>方法初始化了。因為ketchup字段被初始化為一個(gè)編譯時(shí)常量,它被編譯器特殊處理了。 4.卸載 在很多方面,Java虛擬機中類(lèi)的生命周期和對象的生命周期很相似。虛擬機創(chuàng )建并初始化對象,使程序能使用對象,然后在對象變得不再引用后可選地執行垃圾收集。同樣,虛擬機裝載、連接并初始化類(lèi),使得程序能使用類(lèi),當程序不在引用它們的時(shí)候可選地卸載它們。 類(lèi)的垃圾收集這卸載在Java虛擬機中很重要,是因為Java程序可以在運行時(shí)通過(guò)用戶(hù)自定義的類(lèi)裝載器裝載類(lèi)型來(lái)動(dòng)態(tài)擴展程序,所有被裝載類(lèi)型都在方法區占據內存空間,方法區的不能無(wú)限增長(cháng),所以得收集、卸載。 判斷一個(gè)動(dòng)態(tài)裝載的類(lèi)型是否仍然需要的方法就是:如果程序不再引用某類(lèi)型,那么這個(gè)類(lèi)型就可以被收集。 使用啟動(dòng)類(lèi)裝載器裝載的類(lèi)型永遠是可觸及的,所以永遠不會(huì )被卸載。只有用戶(hù)定義的類(lèi)裝載器裝載的類(lèi)型才會(huì )變成不可觸及的,從而被虛擬機回收。 5.總結 類(lèi)在裝載、連接、初始化以后,這就把一個(gè)類(lèi)型二進(jìn)制數據解析為方法區中的內部數據結構、并在堆上建立了一個(gè)Class對象,類(lèi)變量也已經(jīng)正確初始化,這樣就可以隨時(shí)訪(fǎng)問(wèn)它的靜態(tài)字段,調用靜態(tài)方法或者創(chuàng )建它的實(shí)例(創(chuàng )建對象)。 當類(lèi)(型)在變得不可收集以后,就會(huì )被虛擬機回收。interface Example1f {
int ketchup = 5;
int mustard = (int) (Math.random() * 5.0);
}
聯(lián)系客服