深入Java虚拟机读书笔记第五章Java虚拟机
Java虛擬機
Java虛擬機之所以被稱之為是虛擬的,就是因為它僅僅是由一個規范來定義的抽象計算機。因此,要運行某個Java程序,首先需要一個符合該規范的具體實現。
Java虛擬機的生命周期
一個運行時的Java虛擬機實例的天職就是:負責運行一個Java程序。當啟動一個Java程序時,一個虛擬機實例就誕生了。當該程序關閉退出,這個虛擬機實例也就隨之消亡。每個Java程序都運行于它自己的Java虛擬機實例中。
Java虛擬機實例通過調用某個初始類的main()方法來運行一個Java程序。而這個main()方法必須是共有的public、靜態的static、返回值為void,并且接受一個字符串數組作為參數。任何擁有這樣一個main()方法的類都可以作為Java程序運行的起點。
比如,考慮這樣一個Java程序,它打印出傳給它的命令行參數:
Class?Echo{
??Public?static?void?main(String[]?args){
????Int?len?=?args.length;
????For(int?i?=?0;?i?<?len;?++i){
??????System.out.print(args[i]?+?“?”);
????}
????System.out.println();
??}
}
必須告訴Java虛擬機要運行的Java程序中初始類的名字,這個程序將從它的main()方法開始運行。如在Windows上使用命令:
Java?Echo?Greetings,?Planet.
Java程序初始類中的main()方法,經作為該程序初始線程的起點,任何其他的線程都是由這個線程啟動的。
在Java虛擬機內部有兩種線程:守護線程和非守護線程。守護線程通常是由虛擬機自己用的,比如執行垃圾收集任務的線程。但是,Java程序也可以把它創建的任何線程標記為守護線程。而Java程序中的初始線程---就是開始于main()的那個,是非守護線程。
只要還有任何非守護線程在運行,那么這個Java程序也在繼續運行(虛擬機仍然存活)。當該程序所有的非守護線程都終止時,虛擬機實例將自動退出。假若安全管理器運行,程序本身也能夠通過調用Runtime類或者System類的exit()方法退出。
Java虛擬機的體系結構
在Java虛擬機規范中,一個虛擬機實例的行為是分別按照子系統、內存區、數據類型以及指令這幾個術語來描述的。
下圖為Java虛擬機的結構框圖,包括在規范中描述的主要子系統和內存區。前面提到,每個Java虛擬機都有一個類裝載器子系統,它根據給定的全限定名來裝入類型。同樣,每個Java虛擬機都有一個執行引擎,它負責執行那些包含在被裝載類的方法中的指令。
?
?
當Java虛擬機運作一個程序時,它需要內存來存儲許多東西,例如,字節碼,從已裝載的class文件中得到的其他信息,程序創建的對象、傳遞給方法的參數、返回值、局部變量以及運算的中間結果等,Java虛擬機把這些東西都組織到幾個“運行時數據區”中,以便于管理。Java虛擬機規范對“運行時數據區”的描述是抽象的,由具體實現的設計者決定。
某些運行時數據區是由程序中所有線程共享的,還有一些則只能由一個線程擁有。每個Java虛擬機實例都有一個方法區以及一個堆,它們是由該虛擬機實例中所有線程共享的。當虛擬機裝載一個class文件時,它會從這個class文件包含的二進制數據中解析類型信息。然后,它把這些類型信息放到方法區中。當程序運行時,虛擬機會把所有該程序在運行時創建的對象都放到堆中。
當每一個新線程被創建時,它都將得到它自己的PC寄存器(程序計數器)以及一個Java棧:如果線程正在執行的是一個Java方法(非本地方法),那么PC寄存器的值將總是指示下一條將被執行的指令,而它的Java棧則總是存儲該線程中Java方法調用的裝載---包括它的局部變量,被調用時傳進來的參數,它的返回值,以及運算的中間結果等。而本地方法調用的狀態,則是以某種依賴于具體實現的方式存儲在本地方法棧中,也可能是在寄存器或者其他某些與特定實現相關的內存中。
Java棧是由許多棧幀(stack frame)或幀(frame)組成的,一個棧幀包含一個Java方法調用的狀態。當線程調用一個Java方法時,虛擬機壓入一個新的棧幀到該線程的Java棧中,當該方法返回時,這個棧幀從Java棧中彈出。
Java虛擬機沒有寄存器,其指令集使用Java棧來存儲中間數據。這樣設計的原因是為了保持Java虛擬機的指令集盡量緊湊,同時也便于Java虛擬機在那些只有很少通用寄存器的平臺上實現。另外Java虛擬機的這種基于棧的體系結構也有助于運行時某些虛擬機實現的動態編譯器和即時編譯器的代碼優化。
下圖描繪了Java虛擬機為每一個線程創建的內存區,這些內存區域是私有的,任何線程都不能訪問另一個線程的PC寄存器或者Java棧。
?
?數據類型
????Java虛擬機是通過某些數據類型來執行計算的,數據類型及其運算都是由Java虛擬機規范嚴格定義的,數據類型可以分為兩種:基本類型和引用類型?;绢愋偷淖兞砍钟性贾?#xff0c;而引用類型的變量持有引用值。術語“引用值”指的是對某個對象的引用,而不是該對象的本身,與此相對,原始值則是真正的原始數據。
?
Java語言中的所有基本類型同樣也都是Java虛擬機中的基本類型。但是boolean有點特別,雖然Java虛擬機也把boolean看做基本類型,但是指令集對boolean只有很有限的支持。當編譯器把Java源碼編譯為字節碼時,它會用int或byte來表示boolean。在Java虛擬機中,false是由整數零來表示的,所有非零整數都表示true,涉及boolean值的操作則會使用int。另外,boolean數組是當做byte數組來訪問的,但是在“堆”區,它也可以被表示為位域。
Java虛擬機中還有一個只在內部使用的基本類型:returnAddress,Java程序猿不能使用這個類型,這個基本類型被用來實現Java程序的finally字句。
Java虛擬機的引用類型被統稱為“引用”(reference),有三種引用類型:類類型、接口類型以及數組類型,它們的值都是對動態創建對象的引用。類類型的值是對類實例的引用;數組類型的值是對數組對象的引用,在Java虛擬機中,數組是個真正的對象;而接口類型的值,則是對實現了該接口的某個類實例的引用。
Java虛擬機規范定義了每一種數據類型的取值范圍,但是卻沒有定義它們的位寬。位寬由具體的虛擬機實現設計者決定。
字長的考量
Java虛擬機中,最基本的數據單元就是字,它的大小是由每個虛擬機實現的設計者來決定的。字長必須足夠大,至少是一個字單元就足以持有byte、short、int、char、float、returnAddress或者reference類型的值,而兩個字單元就足以持有long或者double類型的值。因此,虛擬機實現的設計者至少得選擇32位作為字長,或者選擇更為高效的字長大小。通常根據底層主機平臺的指針長度來選擇字長。
在Java虛擬機規范中,關于運行時數據區的大部分內容,都是基于“字”這個抽象概念的。比如,關于棧幀的兩個部分---局部變量和操作數棧---都是按照“字”來定義的。這些內存區能夠容納任何虛擬機數據類型的值,當把這些值放到局部變量或者操作數棧中時,它將占用一個或兩個字單元。
在運行時,Java程序無法偵測到底層虛擬機的字長大小;同樣,虛擬機的字長大小也不會影響程序的行為---它僅僅是虛擬機實現的內部屬性。
?
類裝載子系統
在Java虛擬機中,負責查找并裝載類型的那部分被稱為類裝載子系統。
Java虛擬機有兩種類裝載器:啟動類裝載器和用戶自定義類裝載器。前者是Java虛擬機實現的一部分,后者則是Java程序的一部分。由不同的類裝載器裝載的類將被放在虛擬機內部的不同命名空間中。
類裝載子系統涉及Java虛擬機的其他幾個組成部分,以及幾個來自java.lang庫的類。比如,用戶自定義的類裝載器是普通的Java對象,它的類必須派生自java.lang.ClassLoader類。ClassLoader中定義的方法為程序提供了訪問類裝載器機制的接口。此外,對于每一個被裝載的類型,Java虛擬機都會為它創建一個java.lang.Class類的實例來代表該類型。和所有其他對象一樣,用戶自定義的類裝載器以及Class類的實例都放在內存中的堆區,而裝載的類型信息則都位于方法區。
裝載、連接以及初始化 類裝載子系統除了要定位和導入二進制class文件外,還必須負責驗證被導入類的正確性,為類變量分配并初始化內存,以及幫助解析符號引用。這些動作必須嚴格按以下順序進行:
1、裝載---查找并裝載類型的二進制數據
2、連接---執行驗證,準備,以及解析
驗證---確保被導入類型的正確性
準備--為類變量分配內存,并將其初始化為默認值
解析---把類型中的符號引用轉換為直接引用
3、初始化---把類變量初始化為正確初始值
啟動類裝載器 只要是符合Java?class文件格式的二進制文件,Java虛擬機實現都必須能夠從中辨別并裝載其中的類和接口。某些虛擬機實現也可以識別其他的非規范的二進制格式文件,但它必須能夠辨別class文件。
每個Java虛擬機實現都必須有一個啟動類裝載器,它知道怎么裝載受信任的類,比如Java?API的class文件。Java虛擬機規范并未規定啟動類裝載器如何去尋找class文件。
只要給定某個類型的全限定名,啟動類裝載器就必須能夠以某種方法得到定義該類型的數據。在JDK1.2中,啟動類裝載器只在系統類(Java?API的類文件)的安裝路徑中查找要裝入的類;而搜索CLASSPATH目錄的任務,現在交給了系統類裝載器---它是一個自定義的類裝載器,當虛擬機啟動時就被自動創建。
用戶自定義類裝載器????盡管“用戶自定義類裝載器”本身是Java程序的一部分,但類ClassLoader中的四個方法是通往Java虛擬機的通道:
protected?final?class?defineClass(String?name,?byte?data[],?int?offset,?int?length)
protected?final?class?defineClass(String?name,?byte?data[],?int?offset,?int?length,?ProtectionDomain?protectionDomain)
protected?final?Class?findSystemClass(String?name);
protected?final?void?resolveClass(Class?c);
任何Java虛擬機實現都必須把這些方法連接到內部的類裝載器子系統中。
兩個被重載的defineClass()方法都要接受一個名為data[]的字節數組作為輸入參數,并且在data[offset]到data[offset+length]之間的二進制數據必須符合Java?class文件格式---它表示一個新的可用類型。而name參數是個字符串,它給出指定類型的全限定名。使用第一個defineClass()時,該類型被賦以默認的保護域,使用第二個時該類型的保護域由它的protectionDomain參數指定。每個Java虛擬機實現都必須保證ClassLoader類的defineClass()方法能夠把新類型導入到方法區中。
findSystemClass()方法接受一個字符串作為參數,它指出被裝入類型的全限定名。在版本1.2中,該方法使用系統類裝載器來裝載指定類型。任何Java虛擬機實現都必須保證findSystemClass()方法能夠以這種方式調用系統類裝載器。
resolveClass()方法接受一個Class實例的引用作為參數,它將對該Class實例表示的類型執行連接動作。而defineClass()方法則只負責裝載。當defineClass方法返回一個Class實例時,也就表示指定的class文件已經被找到并裝載到方法區了,但是卻不一定被連接和初始化了。Java虛擬機實現必須保證ClassLoader類的resolveClass方法能夠讓類裝載器子系統執行連接動作。
命名空間 每個類裝載器都有自己的命名空間,其中維護著由它裝載的類型。一個Java程序可以多次裝載具有同一個全限定名的多個類型,當多個類裝載器都裝載了同名的類型時,為了唯一地標識該類型,還要在類型名稱前加上裝載該類型的類裝載器的標識。
Java虛擬機中的命名空間,其實是解析過程的結果。對于每一個被裝載的類型,Java虛擬機都會記錄裝載它的類裝載器。當虛擬機解析一個類到另一個類的符號引用時,它需要被引用類的類裝載器。
方法區
????在Java虛擬機中,關于被裝載類型的信息存儲在一個邏輯上被稱為方法區的內存中。當虛擬機裝載某個類型時,它使用類裝載器定位相應的class文件,然后讀人這個class文件---一個線性二進制數據流---然后將它傳輸到虛擬機中、.緊接著虛擬機提取其中的類型信息,并將這些信息存儲到方法區。該類型中的類(靜態)變量同樣也是存儲在方法區。Java虛擬機在內部如何存儲類型信息,這是由具體實現的設計者來決定的。
當虛擬機運行Java程序時,它會查找使用存儲在方法區中的類型信息。設計其應當為類型信息的內部表示設計適當的數據結構,以盡可能在保持虛擬機小巧緊湊的同時加快程序的運行效率。如果正在設計一個需要在少量內存的限制中操作的實現,設計者可能會決定以犧牲某些運行速度來換取緊湊性。另外一方面,如果設計一個將在虛擬內存系統中運行的實現,設計者可能會決定在方法區中保存一些冗余倍息,以此來加快執行速度。(如果底層主機沒有提供虛擬內存,但是提供了一個硬盤,設計者可能會在實現中創建一個虛擬內存系統。Java虛擬機的設計者可以根據目標平臺的資源限制和需求,在空問和時間上做出權衡.選擇實現什么樣的數據結構和數據組織。
由于所有線程都共享方法區,因此它們對方法區數據的訪問必須被設計為是線程安全的。比如,假設同時有兩個線程都企圖訪問一個名為Lava的類,而這個類還沒有被裝人虛擬機,那么,這時只應該有一個線程去裝載它,而另一個線程則只能等待。方法區的大小不必是固定的,虛擬機可以根據應用的需要動態調整。同樣,方法區也不必是連續的,方法區可以在一個堆(甚至是虛擬機自己的堆)中自由分配。另外,虛擬機也可以允許用戶或者程序員指定方法區的初始大小以及最小和最大尺寸等。
方法區也可以被垃圾收集,因為虛擬機允許通過用戶定義的類裝載器來動態擴展Java程序,因此一些類也會成為程序“不再引用”的類。當某個類變為不再被引用的類時,Java虛擬機可以卸載這個類(垃圾收集)從而使方法區占據的內存保持最小。
類型信息 對每個裝栽的類型,虛擬機都會在方法區中存儲以下類型信息:
?這個類型的全限定名。
?這個類型的直接超類的全限定名(除非這個類型是java.lang.Object,它沒有超類)
?這個類型是類類型還是接口類型
?這個類型的訪問修飾符(public、abstract或final的某個子集)
?任何直接超接口的全限定名的有序列表。
在Java?class文件和虛擬機中,類型名總是以全限定名出現在Java源代媽中,全限定名由類所屬包的名稱加一個再加一個“.”,再加上類名組成。例如,類Object的所屬包為java.lang,那它的全限定名應該是java.lang.Object,但在class文件里,所有的“.”都被斜杠“/”代替.這樣就成為java/lang/Objectc。至于全限定名在方法區中的表示,則因不同的設計者有不同的選擇而不同,可以用任何形式和數據結構來代表。
除了上面列出的基本類型息外,虛擬機還得為每個被裝載的類型存儲以下信息:
?該類型的常量池。
?字段信息。
?方法信息
?除了常量以外的所有類(靜態)變量。
?一個到類ClassLoader的引用。
?一個到Class類的引用。
常量池 虛擬機必須為每個被裝載的類型維護一個常量池。常量池就是該類型所用常量的一個有序集合,包括直接常量(string、integer和floating?point常量)和對其他類型、字段和方法的符號引用。池中的數據項就像數組一樣是通過索引訪問的。因為常量池存儲了相應類型所用到的所有類型、字段和方法的符號引用,所以它在Java程序的動態連接中起著核心的作用。
字段信息????對于類型中聲明的每一個字段,方法區中必須保存下面的信息。除此之外,這些字段在類或接口中的聲明順序也必須保存。下面是字段信息的清單:
?字段名。
?字段的類型。
?字段的修飾符(public、private、protected.、static、final、volatile、transient的某個子集)。
方法信息????對于類型中聲明的每一個方法,方法區中必須保存下面的信息。和字段一樣,這些方法在類或者接口中的聲明順序也必須保存。下面是力法信息的清單:
?方法名。
?方法的返回類型(或void)
?方法參數的數量和類型(按聲明順序).
?方法的修飾符(public、private、protected、static,?find、synchronized、native、abstract的某個子集)。
除上面的清單中列出的條目之外,如果某個方法不是抽象的和本地的,它還必須保存下列信息:
?方法的字節碼(bytecodes)。
?操作數棧和該方法的棧幀中的局部變量區的大小。
?異常表。
類(靜態)變量類變量是由所有類實例共享的,但是即使沒有任何類實例,它也可以被訪問。這些變量只與類有關——而非類的實例,因此它們總是作為類型信息的一部分而存儲在方法區。除了在類中聲明的編譯時常量外,虛擬機在使用某個類之前,必須在方法區中為這些類變量分配空間。
而編譯時常量(就是那些用final聲明以及用編譯時已知的值初始化的類變量)則和一般的類變量的處理方式不同,每個使用編譯時常量的類型都會復制它的所有常量到自己的常量池中,或嵌人到它的字節碼流中。作為常量池或字節碼流的一部分,編譯時常量保存在方法區中——就和一般的類變量一樣。但是當一般的類變量作為聲明它們的類型的一部分數據面保存的時候,編譯時常量作為使用它們的類型的一部分而保存。
????指向ClassLoader類的引用????每個類型被裝載的時候,虛擬機必須跟蹤它是由啟動類裝載器還是由用戶自定義類裝載器裝載的。如果是用戶自定義類裝載器裝載的,那么虛擬機必須在類型信息中存儲對該裝載器的引用。這是作為方法表中的類型數據的一部分保存的。
虛擬機會在動態連接期間使用這個信息。當某個類型引用另一個類型的時候,虛擬機會請求裝載發起引用類型的類裝載器來裝載被引用的類型。這個動態連接的過程,對于虛擬機分離命名空間的方式也是至關重要的。為了能夠正確地執行動態連接以及維護多個命名空間,虛擬機需要在方法表中得知每個類都是由哪個類裝載器裝載的。
指向Class類的引用????對于每一個被裝載的類型(不管是類還是接口)虛擬機都會相應地為它創建一個java.lang.Class類的實例,而且虛擬機還必須以某種方式把這個實例和存儲在方法區中的類型數據關聯起來。在你的Java程序中,你可以得到并使用指向Class對象的引用。Class類中的一個靜態方法可以讓用戶得到任何己裝載的類的Class實例的引用。
public?static?Class?forName(String?classHame)??//連接數據庫常用此方法
比如,如果調用forName("java.lang.Object"),那么將得到一個代表java.lang.Object的Class對象的引用。如果調用forName("java.util.Enumeration"),那么得到的是代表java.util包中java.util.Enumeration接口的Class對象的引??梢允褂胒orName()來得到代表任何包中任何類型的Class對象的引用,只要這個類型可以被(或者已經被)裝載到當前命名空間中。如果虛擬機無法把請求的類型裝載到當前命名空間,那么forName?()會拋出ClassNotFoundException異常。
另一個得到Class對象引用的方法是,可以調用任何對象引用的getClass()方法。這個方法被來自Object類本身的所有對象繼承:
Public?final?class?getClass();
? 比如,如果你有一個到java.lang.Integer類的對象的引用,那么你只需簡單地調用Integer對象引用的getClass()方法,就可以得到表不java.lang,Integer類的Class對象。給出一個指向Class對象的引用,就可以通過Class類中定義的方法來找出這個類型的相關信息。如果查看這些方法,會很快意識到,Class類使得運行程序可以訪問方法區中保存的信息。
下面是Class類中生明的方法:
public?String?getNameO;
public?Class?getSuperClass();
public?boolean?islnterface();
public?Class[]?getlnterface();
public?ClassLoader?getClassLoader?();
這些方法僅能返回已裝載類型的信息。getName()返回類型的全限定名,getSuperChss()返回類型的直接超類的Class實例。如果類型是java.lang.Object類或者是一個接口,它們都沒有超類,getSuperClass()返回null。Islntcrface()判斷該類型是否是接口,如果Class對象描述一個接口就返回true;如果它描述一個類則返回false。getlnterfaces()返回一個Class對象數組,其中每個Class對象對應一個直接超接口,超接口在數組中以類型聲明超接口的順序出現。如果該類型沒有直接超接口,getlnterfaces()則返回一個長度為零的數紐。getClassLoader()返回裝載該類型的ClassLoadeT對象的引用,如果類型是由啟動類裝載器裝載的,則返回null。所有這些信息都直接從方法區中獲得。
方法表????為了盡可能提高訪問效率,設計者必須仔細設計存儲在方法區中的類型信息的數據結構,因此,除了以上討論的原始類型信息,實現中還可能包括其他數據結構以加快訪問原始數據的速度,比如方法表。虛擬機對每個裝載的非抽象類,都生成一個方法表,把它作為類信息的一部分保存在方法表中。方法表是一個數組,它的元素是所有它的實例可能被調用的實例方法的直接引用,包括那些從超類繼承過來的實例方法。(對于抽象類和接口,方法表沒有什么幫助,因為程序決不會生成它們的實例。)運行時可以通過方法表快速搜尋在對象中調用的實例方法。
方法區使用示例,為了展示虛擬機如何便用方法表中的信息,我們舉個例子,看下面這個類:
Class?Lava?{
Private?int?speed?=?5;
Void?flow()?{
??}
}
?
Class?Volcano{
??Lava?lava?=?new?Lava();
??lava.flow();
}
下面的段落描述了某個實現中是如何執行Volcano程序中main()方法的字節碼中第一條指令的。不同的虛擬機實現可能會用完全不同的方法來操作,下面描述的只是其中一種可能——但并不是僅有的一種,下面看一下Java虛擬機是如何執行Volcano程序中main?()方法的第一條指令的。
要運行Vokano程序,首先得以某種“依賴于實現的”方式告訴虛擬機“Volcano”這個名字。之后,虛擬機將找到并讀人相應的class文件“Volcano.class”,然后它會從導人的class文件里的二進制數據中提取類型信息并放到方法區中。通過執行保存在方法區中的字節碼,虛擬機開始執行main()方法,在執行時,它會一直持有指向當前類(Volcano類)的常量池(方法區中的一個數據結構)的指針。
注意,虛擬機開始執Volcano類中main()方法的字節碼的時候,盡管Lava類還沒被裝載,但是和大多數(.也許所有)虛擬機實現一樣,它不會等到把程序中用到的所有類都裝載后才開始運行程序。恰好相反,它在需要時才裝載相應的類。main()的第一條指令告知虛擬機為列在常量池第一項的類分配足夠的內存。所以虛擬機使用指向Volcano常量池的指針找到第一項,發現它是一個對Lava類的符號引用,然后它就檢查方法區,看Lava類是否已經被裝載了。
這個符導引用僅僅是一個給出類Lava的全限定名“Lava”的字符串。為了能讓虛擬機盡可能快地從一個名稱找到類,設計者應當選擇最佳的數據結構和算法。這里可以采用各種方法,如散列表,搜索樹等等。同樣的算法也可以用于實現Class類的forName()方法,這個方法根據給定的全限定名返同Class引用。
當虛擬機發現還沒有裝載過名為“Lava”的類時,它就開始査找并裝載文件“Lava.class”,并把從讀人的二逬制數據中提取的類型信息放在方法區中。緊接著,虛擬機以一個直接指向方法區Lava類數據的指針來替換常量池第一項(就是那個字符串“Lava”)——以后就可以用這個指針來快速地訪問Lava類。這個替換過揮稱為常量池解析,即把常量池中的符號引用替換為直接引用。這是逋過在方法區中搜索被引用的元素實現的,在這期間可能又需要裝載其他類。在這里,我們替換掉符號引用的“直接引用”是一個本地指針。
終于,虛擬機準備為一個新的Lava對象分配內存。此吋,它又需要方法區中的信息。還記得剛剛放到Volcano類常量池第一項的指針嗎?現在虛擬機用它來訪問Lava類型倍息(此前剛放到方法區中的),找出其中記錄的這樣一個信息:一個Lava對象需要分配多少堆空間。
Java虛擬機總能夠通過存儲于方法區的類型信息來確定一個對象需要多少內存,但是,某個特定對象事實上需要多少內存,是跟特定實現相關的。對象在虛擬機內部的表示是由實現的設計者來決定的。
當Java虛擬機確定了一個Lava對象的大小后,它就在堆上分配這么大的空間,并把這個對象實例的變量speed切始化為默認初始值0。假如Lava類的超類Object也有實例變量,則也會在此時被初始化力相應的默認值。
當把新生成的Lava對象的引用壓到棧中,main()方法的第—條指令也完成了。接下來的指令通過這個引用調用Java代碼(該代碼把speed變量初始化為正確初始值5)。另外一條指令將用這個引用調用Lava對象引用的flow()方法。
堆
Java程序在運行時創建的所有類實例或數組都放在同一個堆中。而一個Java虛擬機實例中只存在一個堆空間,因此所有線程都將共享這個堆。又由于一個Java程序獨占一個Java虛擬機實例,因而每個Java程序都有它自己的堆空間——它們不會彼此干擾。但是同一個Java程序的多個線程卻共享著同一個堆空間,在這種情況下,就得考慮多線程訪問對象(堆數據)的同步問題了。
Java虛擬機有一條在堆中分配新對象的指令,卻沒存釋放內存的指令。正如你無法用Java代碼去明確釋放一個對象一樣,字節碼指令也沒有對應的功能。虛擬機自己負責決定如何以及何時釋放不再被運行的程序引用的對象所占據的內存。程序本身不用去考慮何時需回收對象所占用的內存,通常,虛擬機把這個任務交給垃圾收集器。
垃圾收集????垃圾收集器的主要工作就是自動回收不再被運行的程序引用的對象所占用的內存。此外,它也可能去移動那些還在使用的對象,以此減少堆碎片。
Java虛擬機規范并沒有強制規定垃圾收集器,它只要求虛擬機實現必須“以某種方式”管理自己的堆空間。舉個例子,某個實現可能只有固定大小的堆空問可用,當空間填滿,它就簡單地拋出OutOfMemory異常,根本不去考慮回收垃圾對象的問題。這樣的一個實現雖然簡陋,擔卻是符合規范的。總之,Java虛擬機規范并沒有規定具體的實現必須為Java程序準備多少內存,也沒有說它必須怎么管理自已的堆空間,它僅僅告訴實現的投計者:Java稈序需要從堆中為對象分配空間,并且程序本身不會主動釋放它。因此堆空間的管理(包括垃圾收集)問題得由設計者自行去考慮處理方式。
Java虛擬機規范沒有指定垃圾收集應該采用什么技術。這些都由虛擬機的設計者根據他們的目標、考慮所受的限制、用自己的能力去決定什么才是最好的技術。因為到對象的引用可能很多地方都存在,如Java棧、堆、方法區、本地方法棧,所以垃圾收集技術的使用在很大程度上會影響到運行時數據區的設計。
和方法區一樣,堆空間也不必是連續的內存區。在程序運行時,它可以動態擴展或收縮。事實上,一個實現的方法區可以在堆頂實現。換句話說,就是虛擬機需要為一個新裝載的類分配內存時,類型信息和實際對象可以都在同一個堆上。因此,負責回收無用對象的垃圾收集器可能也要負責無用類的釋放(卸載)。另外,某些實現可能也允許用戶或程序員指定堆的初始大小、最大最小值等等。
對象的內部表示????Java虛擬機規范并沒有規定lava對象在堆中是如何表示的。對象的內部表示也影響著整個堆以及垃圾收集器的設計,它由虛擬機的實現者決定。
Java對象中包含的基本數據由它所屬的類及其所有超類聲明的實例變量組成。只要有一個對象引用,虛擬機就必須能夠快速地定位對象實例的數據。另外,它也必須能通過該對象引用訪問相應的類數據(存儲于方法區的類型信息)。因此在對象中通常會有一個指向方法區的指針。一種可能的堆空間設計就是,把堆分為兩部分:一個句棲池,一個對象池,如圖5-5所示。而一個對象引用就是一個指向句棲池的本地指針。句柄池的每個條目有兩部分:一個指向對象實例變量的指針,一個指向方法區類型數據的指針。這種設計的好處是有利于堆碎片的整理,當移動對象池中的對象時,句柄部分只需耍更改一下指針指向對象的新地址就可以了——就是在句柄池中的那個指針。缺點是每次訪問對象的實例變量都要舒過兩次指針傳遞。
另一種設計方式是使對象指針宜接指向一組數據,而讀數據包括對象實例數據以及指向方法區中類數據的指針。這樣設計的優缺點正好與前面的方法相反,它只需要一個指針就可以訪問對象的實例數據,但是移動對象就變得更加復雜。當使用這種堆的虛擬機為了減少內存碎片。而移動對象的時候,它必須在整個運行時數據K中更新指向被移動對象的引用。圖5-6描繪了這種表示對象的方法。
有如下幾個理由要求虛擬機必須能夠通過對象引用得到類(類權)數據:當程序在運行時需要轉換某個對象引用為另一種類型時,虛擬機必須要檢查這種轉換是否被允許,被轉換的對象是否的確足被引用的對象或者它的超類型。當程序在執行instanceof操作時,虛擬機也進行了同樣的檢查。在這兩種情況下,虛擬機部需要查看被引用的對象的類數據。最后,當程序中調用某個實例方法時,虛擬機必須迸行動態綁定,換句話說,它不能按照引用的類型來決走將要調用的方法.而必須報據對象的'實際類。為此,虛戒機必須再次通過對象的引用去訪問類數據。
不管虛擬機的實現使用什么樣的對象表示法,很可能每個對象都有一個方法表,因為方法表加快了調用實例方法時的效率,從而對Java虛擬機實現的整體性能起著非常重要的正面作用;但是Java虛擬機規范并未要求必須使用方法表,所以并是所有實現中都會使用它。比如那些有嚴格內存資源限制的實現,或許它們裉本不可能有足夠的額外內存資源來存儲方法展。如果一個實現使用方法表,那么僅僅使用一個指向對象的引用,就可以很快地訪問到對象的方法表。
?
下圖展示了一種把方法表和對象引用聯系起來的實現方式。每個對象的數據都包含一個指向特殊數據結構的指針,這個數據結構位于方法區,它包括兩部分:
?一個指向方法區對應類數據的指針。
?此對象的力法表。
方法表是個指針數組,其中的每一項都是一個指向“實例方法數據”的指針,實例方法可以被那類的對象調用。方法表指向的實例方法數據包括以下信息:
?此方法的操作數棧和局部變里區的大小。
?此方法的字節碼。
?異常表。
這些足夠虛擬機去用一個方法了,方法表中包含有方法指針---指向類或其超類聲明的方法的數據:也就是說,方法表所指向的方法可能是此類聲明的,也可能是它繼承下來的。
堆上的對象數據中還有一個邏輯部分,那就是對象鎖。這是—個互斥對象,虛擬機屮的每個對象都有一個對象鎖,它被用于協調多個線程訪問同一個對象時的同步。在任何時刻,只能有一個線程“擁有”這個對象鎖,因此只有這個線程才能訪問該對象的數據。此時其他希望訪問這個對象的線程只能等待,直到擁有對象鎖的線程釋放鎖。當某個線程擁有一個對象鎖后,可以繼續對這個鎖追加請求。但請求幾次,必須對應地釋放幾次,之后才能輪到其他線程。比如一個線程清求了三次鎖,在它釋放三次鎖之前,它一直保持“擁有”這個鎖。
?
很多對象在其整個生命周期內都沒有被任何線程加鎖。在線程實際請求某個對象的鎖之前,實現對象鎖所需要的數據是不必要的。很多實現不在對象自身內部保存一個指向鎖數據的指針。而只有當第一次需要加鎖的時候才分配對應的鎖數據,但這時虛擬機要用某種間接方法來聯系對象數據和對應的鎖數據,例如把鎖數據放在一個以對象地址為索引的搜索樹中。
除了實現鎖所需要的數據外,每個Java對象邏輯上還與實現等待集合(wait??set)的數據相關聯。鎖是用來實現多個線程對共享數據的互斥訪問的,而等待集合是用來讓多個線程為完成一個共同目標而協調工作的。
等待集合由等待方法和通知方法聯合使用。每個類都從Object那里繼承了三個等待方法(三個名為wait()的重載方法)和兩個通知方法(notify()及notifyAll())。當某個線程在一個對象上調用等待方法吋,虛擬機就阻塞這個線程,并把它放在了這個對象的等待集合中。直到另一個線程在同一個對象上調用通知方法,虛擬機會在之后的某個時刻喚醒一個或多個在等待集合中被阻塞的線程。正像鎖數據一樣,在實際調用對象的等待方法或通知方法之前,實現對象的等待集合的數椐并不是必需的。因此,許多虛擬機實現都把等待集合數據與實際對象數據分開,只有在需要時才為此對象創建同步數據(通常是在第一次調用等待方法或通知方法時)。
最后一種數據類型——可以作為堆中某個對象映像的一部分,是與拉圾收集器有關的數據。垃圾收集器必須(以某種方式)跟蹤程序引用的每個對象,這個任務不可避免地要附加一些數據給這些對象,數據的類型要視拉圾收集使用的算法而定。例如,假如垃圾收集器使用“標記并清除”算法,這就需要能夠標記對象能否被引用。此外,對于不再被引用的對象,還需要指明它的終結方法(finalize)是否已經運行過了。像線程鎖一樣,這些數據也可以放在對象數據外。有一些垃圾收集技術只在垃圾收集器運行時需要額外數據。例如“標記并清除”算法就使用一個獨立的位圖來標記對象的引用情況。
除了標記對象的引用情況外,垃圾收集器還要區分對象是否調用了終結方法。對干在其類中聲明了終結方法的對象,在回收它之前,垃圾收集器必須調用它的終結方法。Java語言規范指出,拉圾收集器對每個對象只能調用一次終結方法,但是允許終結方法復活(resurrect)這個對象,即允許該對象被再次引用。這樣當這個對象再次被回收時,就不用再調用終結方法了。需要終結方法的對象不多,而需要復活的更少,所以對一個對象回收兩次的情況很少見。這種用來標志終結方法的數據雖然邏輯上是對象的一部分,但通常實現上不隨對象保存在堆中。大部分情況下,垃圾收集器會在一個單獨的空間保存這個信息。
數組的內部表示 在Java中,數組是真正的對象。和其他對象一樣,數組總是存儲在堆中。同樣,和普通對象一樣,實現的設計者將決定數組在堆中的表示形式。
和其他所有對象一樣,數組也擁有一個與它們的類相關聯的Class實例,所有具有相同維度和類型的數組都是同一個類的實例,而不管數組的長度(多維數組每一維的長度)是多少,例如一個包含3個int整數的數組和一個包含300個int整數的數組擁有同一個類。數組的長度只與實例數據有關。
數組類的名稱由兩部分組成:每一維用一個方括號“[”表示,用字符或字符串表示類型。比如,元素類型為int整數的、一維數組的類名為“[I”,元素類型為byte的三維數組為“[[[B”,元素類型為Object的二維數組“[[Ljava/lang/Object”。
多維數組被表示為數組的數組。比如,int類型的二維數組,將表示為一個一維數組,其中的毎個元素是一個一維int數組的引用。
在堆中的每個數組對象還必須保存的數據是數組的長度、數組數據,以及某些指向數組的類數據的引用。虛擬機必須能夠通過一個數組對象的引用得到此數組的長度,通過索引訪問其元素(其間要檢查數組邊界是否越界),調用所有數組的直接超類Object聲明的方法等等。
程序計數器
對于一個運行中的Java程序而言,其中的每一個線程都有它自己的PC(程序計數器)寄存器,它是在該線程啟動時創建的。PC寄存器的大小是一個字長,因此它既能夠持有一個本地指針,也能夠持有一個returnAddress。當線程執行某個Java方法時,PC寄存器的內容總是下一條將被執行指令的“地址”,這里的“地址”可以是一個本地指針,也可以是在方法字節碼中相對于該方法起始指令的偏移量。如果該線程正在執行一個本地方法,那么此PC寄存器的值是“undefined'”。
Java棧
每當啟動一個新線程時,Java虛擬機都會為它分配一個Java棧。前面我們曾經提到,Java棧以幀為單位保存線程的運行狀態。虛擬機只會直接對Java棧執行兩種操作:以幀為單位的壓?;虺鰲?/span>。某個線程正在執行的方法被稱為該線程的當前方法,當前方法使用的幀棧稱為當前幀,當前方法所屬的類稱為當前類,當前類的常量池稱為當前常量池。在線程執行一個方法時,它會跟蹤當前類和當前常量池。此外,當虛擬機遇到棧內操作指令時,它對當前幀內數據執行操作。
每當線程調用一個Java方法時,虛擬機都會在該線程的Java棧中壓人一個新幀。而這個新幀自然就成為了當前偵。在執行這個方法時,它使用這個幀來存儲參數、局部變量、中間運算結果等等數據。
Java方法可以以兩種方式完成。一種通過return返回的,稱為正常返回;一種是通過拋出異常而異常中止的。不管以哪種方式返回,虛擬機都會將當前幀彈出Java棧然后釋放掉,這樣上—個方法的幀就成為當前幀了。
Java棧上的所有數據都是此線程私有的。任何線程都不能訪問另一個線程的棧數據,因此我們不需要考慮多線程情況下棧數據的訪問同步問題。當一個線程調用一個方法時,方法的局部變量保存在調用線程Java找的幀中。只有一個線程能總是訪問那些局部變最,即調用方法的線程。
像方法區和堆一樣,Java棧和幀在內存中也不必是連續的。幀可以分布在連續的棧里,也可以分布在堆里,或者二者兼而有之。表示Java棧和棧幀的實際數據結構由虛擬機的實現者決定,某些實現允許用戶指定Java棧的初始大小和最大最小值。
棧幀
棧幀由三部分組成:局部變量區、操作數棧和幀數據區。局部變量區和操作數棧的大小要視對應的方法而定,它們是按字長計算的。編譯器在編譯時就確定了這些值并放在class文件中。而幀數據區的大小依賴于具體的實現。
當虛擬機調用一個Java方法時,它從對應類的類型信息中得到此方法的局部變量區和操作數棧的大小,并據此分配棧幀內存,然后壓入Java棧中。
局部變量區??Java棧幀的局部變量區被組織為一個以字長為單位、從0開始計數的數組。字節碼指令通過從0開始的索引來使用其中的數據類型。類型為int、float、reference和returnAddress的值在數組中只占據一項,而類型為byte、short和char的值在存入數組前都將被轉換為int值,因而同樣占據一項。但是類型為long和double的值在數組中卻占據連續的兩項。
在訪問局部變量中的long和double值的時候,指令只需指出連續兩項中第一項的索引值。例如某個long值占據第3、4項,那么指令會取索引為3的long值。局部變量區的所有值都是字對齊的,long和double這樣占據兩項數組元素的值同樣可以起始于任何索引。
局部變量區包含對應方法的參數和局部變量。編譯器首先按聲明的順序把這些參數放入局部變量數組。
除了Java方法的參數(編譯器首先嚴格按照它們的聲明順序放到局部變量數組中,而對于真正的局部變量,它可以任意決定放置順序,甚至可以用一個索引指代兩個局部變量---比如當兩個局部變量的作出域不重疊時,像下面Example3b中的局部變量i和j就是這種情形:在方法的前半段,在開始生效之前,0號索引的入口可以被用來代表i。在方法的后半段,i經超過了有效作用域,0號入口就可以用來表示j了。
class Example3b{
? public static void runtwoLoops(){
for(int i=0; i < 10; ++i){
?System.out.println(i);
}
for(int j=9; j >=0; --j){
?System.out.println(j);
}
? }
}
和其他運行時內存區一樣,虛擬機的實現者可以為局部變量區設計任意的數據結構。比如對于怎樣把long和double類型的值存儲到兩個數組項中,Java虛擬機規范沒有指定。假如某個虛擬機實現的字長為64位,這時就可以把整個long或double數據放在數組中相鄰兩數組項的低項內,而使高項保持為空。
操作數棧??和局部變量區一樣,操作數棧也是被組織成一個以字長為單位的數組。但是和前者不同的是,它不是通過索引來訪問,而是通過標準的棧操作——壓棧和出?!獊碓L問的。比如,如果某個指令把一個值壓人到操作數棧中,稍后另一個指令就可以彈出這個值來使用。
虛擬機在操作數棧中存儲數據的方式和在局部變量區中是一樣的,如int、long、float、double、reference和returnType的存儲。對于byte、short以及char類型的值在壓入到操作數棧之前,也會被轉換為int。
不同于程序計數器,Java虛擬機沒有寄存器,程序計數器也無法被程序指令直接訪問。Java虛擬機的指令是從操作數棧中而不是從寄存器中取得操作數的,因此它的運行方式是基于棧的而不是基于寄存器的。雖然指令也可以從其他地方取得操作數,比如從字節流中跟隨在操作碼(代表指令的字節)之后的字節中或從常量池中,但是主要還是從操作數棧中獲得操作數。
虛擬機把操作數棧作為它的工作區——大多數指令都要從這里彈出數據,執行運算,然后把結果壓回操作數棧。比如,iadd指令就要從操作數棧中彈出兩個整數,執行加法運算,其結果又壓回到操作數棧中??纯聪旅娴氖纠?#xff0c;它演示了虛擬機是如何把兩個int類型的局部變量相加,再把結果保存到第三個局部變量的:
iload_0??//push?the?int?in?local?variable?0
iload_l??//?push?the?int?in?local?variable?1
iadd?//?pop?two?ints,?add?them,?push?result
istore_2 /7?pop?int,?store?into?local?variable?2
在這個字節碼序列里,前兩個指令iload_0和iload_l將存儲在局部變量區中索引為0和1的整數壓人操作數棧中,其后iadd指令從操作數棧中彈出那兩個整數相加,再將結果壓入操作數棧。第四條指令istore_2則從操作數棧中彈出結果,并把它存儲到局部變量區索引為2的位置。圖5-10詳細表述了這個過程中局部變量和操作數棧的狀態變化,閣中沒有使用的局部變量區和操作數棧區域以空白表示。
幀數據區??除了局部變量區和操作數棧外,Java棧幀還需要一些數據來支持常量池解析、正常方法返回以及異常派發機制,這些信息都保存在Java棧幀的幀數據區中。
Java虛擬機中的大多數指令都涉及到常量池入口。有些指令僅僅是從常量池中取出數據然后壓人Java棧(這些數據的類型包括int、long、float、double和String);還有些指令使用常暈池的數據來指示要實例化的類或數組、要訪問的字段,或要調用的方法;還有些指令需要常量池中的數據才能確定某個對象是否屬于某個類或實現了某個接口。
每個虛擬機要執行某個需要到常量池數據的指令時,它都會通過幀數掘區中指向常量池的指針來訪問它。以前講過,常景池中對類型、字段和方法的引用在開始時都是符號。當虛擬機在常量池中搜索的時候,如果遇到指向類、接口、字段或者方法的入口,假若它們仍然是符號,虛擬機那時候才會(也必須)進行解析。
除了用于常量池的解析外,幀數據區還要幫助虛擬機處埋Java方法的正常結束或異常中止。如果是通過return正常結束,虛擬機必須恢復發起調用的方法的棧幀,包括設置PC寄存器指向發起調用的方法中的指令---即緊跟著調用了完成方法的指令的下一個指令。假如方法有返回值,虛擬機必須將它壓入到發起調用的方法的操作數棧,為了處理Java方法執行期間的異常退出情況,幀數據區還必須保存一個對此方法異常表的引用。異常表會在第17章深入描述,它定義了在這個方法的字節碼中受catch子句保護的范圍,異常表中的每一項都有一個被catch子句保護的代碼的起始和結束位置(譯者注:即try子句內部的代碼),可能被catch的異常類在常量池中的索引值,以及catch子句內的代開始的位置。
當某個方法拋出異常時,虛擬機根據幀數據區對應的異常表來決定如何處理。如果在異常表中找到了匹配的catch子句,就會把控制權轉交給catch子句內的代碼。如果沒有發現,方法會立即異常終止。然后虛擬機使用幀數據區的信息恢復發起調用的方法的幀,然后在發起調用的方法的上下文中重新拋出同樣的異常。
除了上述信息(支持常量池解析、正常方法返回和異常派發的數據)外,虛擬機的實現者也可以將其他信息放人幀數據區,如用于調試的數據等。
本地方法棧
前面提到的所有運行時數據區都是在Java虛擬機規范中明確定義的,除此之外,對于一個運行中的Java程序而言,它還可能會用到一些跟本地方法相關的數據區。當某個線程調用一個本地方法時,它就進入了一個全新的并且不再受虛擬機限制的世界。本地方法可以通過本地方法接口來訪問虛擬機的運行時數據區,但不止于此,它還可以做任何它想做的事情。比如,它甚至可以直接使用本地處理器中的寄存器,或者直接從本地內存的堆中分配任意數量的內存等等??傊?#xff0c;它和虛擬機擁有同樣的權限(或者說能力)。
本地方法本質上是依賴于實現的,虛擬機實現的設計者們可以自由地決定使用怎樣的機制來讓Java程序調用本地方法。
任何本地方法接口都會使用某種本地方法棧。當線程調用Java方法時,虛擬機會創建一個新的棧幀并壓人Java棧。然而當它調用的是本地方法時,虛擬機會保持Java棧不變,不再在線程的Java棧中壓人新的幀,虛擬機只是簡單地動態連接并直接調用指定的本地方法??梢园堰@看做是虛擬機利用本地方法來動態擴展自己。就如同lava虛擬機的實現在按照其中運行的Java程序的吩附,調用屬于虛擬機內部的另一個(動態連接的)方法。
如果某個虛擬機實現的本地方法接口是使用C連接模型的話,那么它的本地方法棧就是C棧。我們知道,當C程序調用一個C函數時,其棧操作都是確定的。傳遞給該函數的參數以某個確定的順序壓入棧,它的返回值也以確定的方式傳回調用者。同樣,這就是該虛擬機實現中本地方法棧的行為。
很可能本地方法接口需要回調Java虛擬機中的Java方法(這也是由設計者決定的),在這種情形下,該線程會保存本地方法棧的狀態并進人到另一個Java棧。
圖5-13描繪了這種情況,就是當一個線程調用一個本地方法時,本地方法又回調虛擬機中的另一個Java方法。這幅圖展示了Java虛擬機內部線程運行的全景圖。一個線程可能在整個生命周期中都執行Java方法,操作它的Java棧;或者它可能毫無障礙地在Java棧和本地方法棧之間跳轉。
如圖5-13所示,該線程首先調用了兩個Java方法,而第二個Java方法又調用了一個本地方法,這樣導致虛擬機使用了一個本地方法棧。圖中的本地方法棧顯示為一個連續的內存空間。假設這是一個C語言棧,其間有兩個C函數,它們都以包圍在虛線中的灰色塊表示。第一個C函數被第二個Java方法當做本地方法調用,而這個C函數又調用了第二個C函數。之后第二個C函數又通過本地方法接口回調了一個Java方法(第三個Java方法),最終這個Java方法又調用了一個Java方法(它成為圖中的當前方法)。
就像其他運行時內存區一樣,本地方法棧占用的內存區也不必是固定大小的,它可以根據需要動態擴展或者收縮。某些實現也允許用戶或者程序員指定該內存區的初始大小以及最大、最小值。
轉載于:https://www.cnblogs.com/waimai/p/3280185.html
總結
以上是生活随笔為你收集整理的深入Java虚拟机读书笔记第五章Java虚拟机的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Cocos2d-x For WP8]D
- 下一篇: 二叉树层次遍历