虛擬機生命周期
轉載自:https://www.cnblogs.com/like-minded/p/5157667.html
一個運行時的Java虛擬機實例的天職是:負責運行一個java程序。當啟動一個Java程序時,一個虛擬機實例也就誕生了。當該程序關閉退出,這個虛擬機實例也就隨之消亡。如果同一臺計算機上同時運行三個Java程序,將得到三個Java虛擬機實例。每個Java程序都運行於它自己的Java虛擬機實例中。
Java虛擬機實例通過調用某個初始類的main()方法來運行一個Java程序。而這個main()方法必須是共有的(public)、靜態的(static)、返回值為void,並且接受一個字符串數組作為參數。任何擁有這樣一個main()方法的類都可以作為Java程序運行的起點。
public class Test {
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println("Hello World");
}
}
在上面的例子中,Java程序初始類中的main()方法,將作為該程序初始線程的起點,任何其他的線程都是由這個初始線程啟動的。
在Java虛擬機內部有兩種線程:守護線程和非守護線程。守護線程通常是由虛擬機自己使用的,比如執行垃圾收集任務的線程。但是,Java程序也可以把它創建的任何線程標記為守護線程。而Java程序中的初始線程——就是開始於main()的那個,是非守護線程。
只要還有任何非守護線程在運行,那麽這個Java程序也在繼續運行。當該程序中所有的非守護線程都終止時,虛擬機實例將自動退出。假若安全管理器允許,程序本身也能夠通過調用Runtime類或者System類的exit()方法來退出。
JAVA虛擬機的體系結構
下圖是JAVA虛擬機的結構圖,每個Java虛擬機都有一個類裝載子系統,它根據給定的全限定名來裝入類型(類或接口)。同樣,每個Java虛擬機都有一個執行引擎,它負責執行那些包含在被裝載類的方法中的指令。
當JAVA虛擬機運行一個程序時,它需要內存來存儲許多東西,例如:字節碼、從已裝載的class文件中得到的其他信息、程序創建的對象、傳遞給方法的參數,返回值、局部變量等等。Java虛擬機把這些東西都組織到幾個“運行時數據區”中,以便於管理。
某些運行時數據區是由程序中所有線程共享的,還有一些則只能由一個線程擁有。每個Java虛擬機實例都有一個方法區以及一個堆,它們是由該虛擬機實例中所有的線程共享的。當虛擬機裝載一個class文件時,它會從這個class文件包含的二進制數據中解析類型信息。然後把這些類型信息放到方法區中。當程序運行時,虛擬機會把所有該程序在運行時創建的對象都放到堆中。
當每一個新線程被創建時,它都將得到它自己的PC寄存器(程序計數器)以及一個Java棧,如果線程正在執行的是一個Java方法(非本地方法),那麽PC寄存器的值將總是指向下一條將被執行的指令,而它的Java棧則總是存儲該線程中Java方法調用的狀態——包括它的局部變量,被調用時傳進來的參數、返回值,以及運算的中間結果等等。而本地方法調用的狀態,則是以某種依賴於具體實現的方法存儲在本地方法棧中,也可能是在寄存器或者其他某些與特定實現相關的內存區中。
Java棧是由許多棧幀(stack frame)組成的,一個棧幀包含一個Java方法調用的狀態。當線程調用一個Java方法時,虛擬機壓入一個新的棧幀到該線程的Java棧中,當該方法返回時,這個棧幀被從Java棧中彈出並拋棄。
Java虛擬機沒有寄存器,其指令集使用Java棧來存儲中間數據。這樣設計的原因是為了保持Java虛擬機的指令集盡量緊湊、同時也便於Java虛擬機在那些只有很少通用寄存器的平臺上實現。另外,Java虛擬機這種基於棧的體系結構,也有助於運行時某些虛擬機實現的動態編譯器和即時編譯器的代碼優化。
下圖描繪了Java虛擬機為每一個線程創建的內存區,這些內存區域是私有的,任何線程都不能訪問另一個線程的PC寄存器或者Java棧。
上圖展示了一個虛擬機實例的快照,它有三個線程正在執行。線程1和線程2都正在執行Java方法,而線程3則正在執行一個本地方法。
Java棧都是向下生長的,而棧頂都顯示在圖的底部。當前正在執行的方法的棧幀則以淺色表示,對於一個正在運行Java方法的線程而言,它的PC寄存器總是指向下一條將被執行的指令。比如線程1和線程2都是以淺色顯示的,由於線程3當前正在執行一個本地方法,因此,它的PC寄存器——以深色顯示的那個,其值是不確定的。
數據類型
Java虛擬機是通過某些數據類型來執行計算的,數據類型可以分為兩種:基本類型和引用類型,基本類型的變量持有原始值,而引用類型的變量持有引用值。
Java語言中的所有基本類型同樣也都是Java虛擬機中的基本類型。但是boolean有點特別,雖然Java虛擬機也把boolean看做基本類型,但是指令集對boolean只有很有限的支持,當編譯器把Java源代碼編譯為字節碼時,它會用int或者byte來表示boolean。在Java虛擬機中,false是由整數零來表示的,所有非零整數都表示true,涉及boolean值的操作則會使用int。另外,boolean數組是當做byte數組來訪問的,但是在“堆”區,它也可以被表示為位域。
Java虛擬機還有一個只在內部使用的基本類型:returnAddress,Java程序員不能使用這個類型,這個基本類型被用來實現Java程序中的finally子句。該類型是jsr, ret以及jsr_w指令需要使用到的,它的值是JVM指令的操作碼的指針。returnAddress類型不是簡單意義上的數值,不屬於任何一種基本類型,並且它的值是不能被運行中的程序所修改的。
Java虛擬機的引用類型被統稱為“引用(reference)”,有三種引用類型:類類型、接口類型、以及數組類型,它們的值都是對動態創建對象的引用。類類型的值是對類實例的引用;數組類型的值是對數組對象的引用,在Java虛擬機中,數組是個真正的對象;而接口類型的值,則是對實現了該接口的某個類實例的引用。還有一種特殊的引用值是null,它表示該引用變量沒有引用任何對象。
類裝載子系統
在JAVA虛擬機中,負責查找並裝載類型的那部分被稱為類裝載子系統。
JAVA虛擬機有兩種類裝載器:啟動類裝載器和用戶自定義類裝載器。前者是JAVA虛擬機實現的一部分,後者則是Java程序的一部分。由不同的類裝載器裝載的類將被放在虛擬機內部的不同命名空間中。
類裝載器子系統涉及Java虛擬機的其他幾個組成部分,以及幾個來自java.lang庫的類。比如,用戶自定義的類裝載器是普通的Java對象,它的類必須派生自java.lang.ClassLoader類。ClassLoader中定義的方法為程序提供了訪問類裝載器機制的接口。此外,對於每一個被裝載的類型,JAVA虛擬機都會為它創建一個java.lang.Class類的實例來代表該類型。和所有其他對象一樣,用戶自定義的類裝載器以及Class類的實例都放在內存中的堆區,而裝載的類型信息則都位於方法區。
類裝載器子系統除了要定位和導入二進制class文件外,還必須負責驗證被導入類的正確性,為類變量分配並初始化內存,以及幫助解析符號引用。這些動作必須嚴格按以下順序進行:
(1)裝載——查找並裝載類型的二進制數據。
(2)連接——指向驗證、準備、以及解析(可選)。
● 驗證 確保被導入類型的正確性。
● 準備 為類變量分配內存,並將其初始化為默認值。
● 解析 把類型中的符號引用轉換為直接引用。
(3)初始化——把類變量初始化為正確初始值。
每個JAVA虛擬機實現都必須有一個啟動類裝載器,它知道怎麽裝載受信任的類。
每個類裝載器都有自己的命名空間,其中維護著由它裝載的類型。所以一個Java程序可以多次裝載具有同一個全限定名的多個類型。這樣一個類型的全限定名就不足以確定在一個Java虛擬機中的唯一性。因此,當多個類裝載器都裝載了同名的類型時,為了惟一地標識該類型,還要在類型名稱前加上裝載該類型(指出它所位於的命名空間)的類裝載器標識。
方法區
在Java虛擬機中,關於被裝載類型的信息存儲在一個邏輯上被稱為方法區的內存中。當虛擬機裝載某個類型時,它使用類裝載器定位相應的class文件,然後讀入這個class文件——1個線性二進制數據流,然後它傳輸到虛擬機中,緊接著虛擬機提取其中的類型信息,並將這些信息存儲到方法區。該類型中的類(靜態)變量同樣也是存儲在方法區中。
JAVA虛擬機在內部如何存儲類型信息,這是由具體實現的設計者來決定的。
當虛擬機運行Java程序時,它會查找使用存儲在方法區中的類型信息。由於所有線程都共享方法區,因此它們對方法區數據的訪問必須被設計為是線程安全的。比如,假設同時有兩個線程都企圖訪問一個名為Lava的類,而這個類還沒有被裝入虛擬機,那麽,這時只應該有一個線程去裝載它,而另一個線程則只能等待。
對於每個裝載的類型,虛擬機都會在方法區中存儲以下類型信息:
● 這個類型的全限定名
● 這個類型的直接超類的全限定名(除非這個類型是java.lang.Object,它沒有超類)
● 這個類型是類類型還是接口類型
● 這個類型的訪問修飾符(public、abstract或final的某個子集)
● 任何直接超接口的全限定名的有序列表
除了上面列出的基本類型信息外,虛擬機還得為每個被裝載的類型存儲以下信息:
● 該類型的常量池
● 字段信息
● 方法信息
● 除了常量以外的所有類(靜態)變量
● 一個到類ClassLoader的引用
● 一個到Class類的引用
虛擬機生命周期