1 Java虛拟機的生命周期
一個運作時的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) {
System.out.println("Hello World");
}
}
在上面的例子中,Java程式初始類中的main()方法,将作為該程式初始線程的起點,任何其他的線程都是由這個初始線程啟動的。
在Java虛拟機内部有兩種線程:守護線程和非守護線程。守護線程通常是由虛拟機自己使用的,比如執行垃圾收集任務的線程。但是,Java程式也可以把它建立的任何線程标記為守護線程。而Java程式中的初始線程——就是開始于main()的那個,是非守護線程。
隻要還有任何非守護線程在運作,那麼這個Java程式也在繼續運作。當該程式中所有的非守護線程都終止時,虛拟機執行個體将自動退出。假若安全管理器允許,程式本身也能夠通過調用Runtime類或者System類的exit()方法來退出。
2 Java虛拟機的體系結構
2.1 運作時資料區總覽
下圖是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寄存器——以深色顯示的那個,其值是不确定的。
2.1.1 資料類型
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中參數的傳遞有兩種,分别是按值傳遞和按引用傳遞。說一下按引用傳遞。
“當一個對象被當作參數傳遞到一個方法”,這就是所謂的按引用傳遞。
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Test {
public void set(User user){
user.setName("hello world");
user = new User();
user.setName("change");
}
public static void main(String[] args) {
Test test = new Test();
User user = new User();
test.set(user);
System.out.println(user.getName());
}
}
輸出是“hello world”,下面就讓我們來分析一下如上代碼。
User user = new User();
是在堆中建立了一個對象,并在棧中建立了一個引用,此引用指向該對象,如下圖:
test.set(user);
是将引用user作為參數傳遞到set方法,注意:這裡傳遞的并不是引用本身,而是一個引用的拷貝。也就是說這時有兩個引用(引用和引用的拷貝)同時指向堆中的對象,如下圖:
user.setName("hello world");
在set()方法中,“user引用的拷貝”操作堆中的User對象,給name屬性設定字元串"hello world"。如下圖:
user = new User();
在set()方法中,又建立了一個User對象,并将“user引用的拷貝”指向這個在堆中新建立的對象,如下圖:
user.setName("change");
在set()方法中,“user引用的拷貝”操作的是堆中新建立的User對象。
set()方法執行完畢,目光再回到mian()方法
System.out.println(user.getName());
因為之前,"user引用的拷貝"已經将堆中的User對象的name屬性設定為了"hello world",是以當main()方法中的user調用getName()時,列印的結果就是"hello world"。如下圖:
2.2 類裝載子系統
在JAVA虛拟機中,負責查找并裝載類型的那部分被稱為類裝載子系統。
JAVA虛拟機有兩種類裝載器:啟動類裝載器和使用者自定義類裝載器。前者是JAVA虛拟機實作的一部分,後者則是Java程式的一部分。由不同的類裝載器裝載的類将被放在虛拟機内部的不同命名空間中。
類裝載器子系統涉及Java虛拟機的其他幾個組成部分,以及幾個來自java.lang庫的類。比如,使用者自定義的類裝載器是普通的Java對象,它的類必須派生自java.lang.ClassLoader類。ClassLoader中定義的方法為程式提供了通路類裝載器機制的接口。此外,對于每一個被裝載的類型,JAVA虛拟機都會為它建立一個java.lang.Class類的執行個體來代表該類型。和所有其他對象一樣,使用者自定義的類裝載器以及Class類的執行個體都放在記憶體中的堆區,而裝載的類型資訊則都位于方法區。
類裝載器子系統除了要定位和導入二進制class檔案外,還必須負責驗證被導入類的正确性,為類變量配置設定并初始化記憶體,以及幫助解析符号引用。這些動作必須嚴格按以下順序進行:
(1)裝載——查找并裝載類型的二進制資料。
(2)連接配接——指向驗證、準備、以及解析(可選)。
驗證 確定被導入類型的正确性。
準備 為類變量配置設定記憶體,并将其初始化為預設值。
、解析 把類型中的符号引用轉換為直接引用。
(3)初始化——把類變量初始化為正确初始值。
每個JAVA虛拟機實作都必須有一個啟動類裝載器,它知道怎麼裝載受信任的類。
每個類裝載器都有自己的命名空間,其中維護着由它裝載的類型。是以一個Java程式可以多次裝載具有同一個全限定名的多個類型。這樣一個類型的全限定名就不足以确定在一個Java虛拟機中的唯一性。是以,當多個類裝載器都裝載了同名的類型時,為了惟一地辨別該類型,還要在類型名稱前加上裝載該類型(指出它所位于的命名空間)的類裝載器辨別。
2.3 方法區
在Java虛拟機中,關于被裝載類型的資訊存儲在一個邏輯上被稱為方法區的記憶體中。當虛拟機裝載某個類型時,它使用類裝載器定位相應的class檔案,然後讀入這個class檔案——1個線性二進制資料流,然後它傳輸到虛拟機中,緊接着虛拟機提取其中的類型資訊,并将這些資訊存儲到方法區。該類型中的類(靜态)變量同樣也是存儲在方法區中。
JAVA虛拟機在内部如何存儲類型資訊,這是由具體實作的設計者來決定的。
當虛拟機運作Java程式時,它會查找使用存儲在方法區中的類型資訊。由于所有線程都共享方法區,是以它們對方法區資料的通路必須被設計為是線程安全的。比如,假設同時有兩個線程都企圖通路一個名為Lava的類,而這個類還沒有被裝入虛拟機,那麼,這時隻應該有一個線程去裝載它,而另一個線程則隻能等待。
對于每個裝載的類型,虛拟機都會在方法區中存儲以下類型資訊:
- 這個類型的全限定名
- 這個類型的直接超類的全限定名(除非這個類型是java.lang.Object,它沒有超類)
- 這個類型是類類型還是接口類型
- 這個類型的通路修飾符(public、abstract或final的某個子集)
- 任何直接超接口的全限定名的有序清單
除了上面列出的基本類型資訊外,虛拟機還得為每個被裝載的類型存儲以下資訊:
- 該類型的常量池
- 字段資訊
- 方法資訊
- 除了常量以外的所有類(靜态)變量
- 一個到類ClassLoader的引用
- 一個到Class類的引用
2.3.1 常量池
虛拟機必須為每個被裝載的類型維護一個常量池。常量池就是該類型所用常量的一個有序集合,包括直接常量和對其他類型、字段和方法的符号引用。池中的資料項就像數組一樣是通過索引通路的。因為常量池存儲了相應類型所用到的所有類型、字段和方法的符号引用,是以它在Java程式的動态連接配接中起着核心的作用。
2.3.2 字段資訊
對于類型中聲明的每一個字段。方法區中必須儲存下面的資訊。除此之外,這些字段在類或者接口中的聲明順序也必須儲存。
- 字段名
- 字段的類型
- 字段的修飾符(public、private、protected、static、final、volatile、transient的某個子集)
2.3.3 方法資訊
對于類型中聲明的每一個方法,方法區中必須儲存下面的資訊。和字段一樣,這些方法在類或者接口中的聲明順序也必須儲存。
- 方法名
- 方法的傳回類型(或void)
- 方法參數的數量和類型(按聲明順序)
- 方法的修飾符(public、private、protected、static、final、synchronized、native、abstract的某個子集)
除了上面清單中列出的條目之外,如果某個方法不是抽象的和本地的,它還必須儲存下列資訊:
- 方法的位元組碼(bytecodes)
- 操作數棧和該方法的棧幀中的局部變量區的大小
- 異常表
2.3.4 類(靜态)變量
類變量是由所有類執行個體共享的,但是即使沒有任何類執行個體,它也可以被通路。這些變量隻與類有關——而非類的執行個體,是以它們總是作為類型資訊的一部分而存儲在方法區。除了在類中聲明的編譯時常量外,虛拟機在使用某個類之前,必須在方法區中為這些類變量配置設定空間。
而編譯時常量(就是那些用final聲明以及用編譯時已知的值初始化的類變量)則和一般的類變量處理方式不同,每個使用編譯時常量的類型都會複制它的所有常量到自己的常量池中,或嵌入到它的位元組碼流中。作為常量池或位元組碼流的一部分,編譯時常量儲存在方法區中——就和一般的類變量一樣。但是當一般的類變量作為聲明它們的類型的一部分資料面儲存的時候,編譯時常量作為使用它們的類型的一部分而儲存。
2.3.5 指向ClassLoader類的引用
每個類型被裝載的時候,虛拟機必須跟蹤它是由啟動類裝載器還是由使用者自定義類裝載器裝載的。如果是使用者自定義類裝載器裝載的,那麼虛拟機必須在類型資訊中存儲對該裝載器的引用。這是作為方法表中的類型資料的一部分儲存的。
虛拟機會在動态連接配接期間使用這個資訊。當某個類型引用另一個類型的時候,虛拟機會請求裝載發起引用類型的類裝載器來裝載被引用的類型。這個動态連接配接的過程,對于虛拟機分離命名空間的方式也是至關重要的。為了能夠正确地執行動态連接配接以及維護多個命名空間,虛拟機需要在方法表中得知每個類都是由哪個類裝載器裝載的。
2.3.6 指向Class類的引用
對于每一個被裝載的類型(不管是類還是接口),虛拟機都會相應地為它建立一個java.lang.Class類的執行個體,而且虛拟機還必須以某種方式把這個執行個體和存儲在方法區中的類型資料關聯起來。
在Java程式中,你可以得到并使用指向Class對象的引用。Class類中的一個靜态方法可以讓使用者得到任何已裝載的類的Class執行個體的引用。
public static Class<?> forName(String className)
比如,如果調用forName("java.lang.Object"),那麼将得到一個代表java.lang.Object的Class對象的引用。可以使用forName()來得到代表任何包中任何類型的Class對象的引用,隻要這個類型可以被(或者已經被)裝載到目前命名空間中。如果虛拟機無法把請求的類型裝載到目前命名空間,那麼會抛出ClassNotFoundException異常。
另一個得到Class對象引用的方法是,可以調用任何對象引用的getClass()方法。這個方法被來自Object類本身的所有對象繼承:
public final native Class<?> getClass();
比如,如果你有一個到java.lang.Integer類的對象的引用,那麼你隻需簡單地調用Integer對象引用的getClass()方法,就可以得到表示java.lang.Integer類的Class對象。
2.3.7 方法區使用示例
示例為了展示虛拟機如何使用方法區的資訊:
class A {
private int speed = 5;
void run(){}
}
public class B {
public static void main(String[] args){
A a = new A();
a.run();
}
}
不同的虛拟機實作可能會用完全不同的方法來操作,下面描述的隻是其中一種可能——但并不是僅有的一種。
要運作B程式,首先得以某種“依賴于實作的”方式告訴虛拟機“B”這個名字。之後,虛拟機将找到并讀入相應的class檔案“B.class”,然後它會從導入的class檔案裡的二進制資料中提取類型資訊并放到方法區中。通過執行儲存在方法區中的位元組碼,虛拟機開始執行main()方法,在執行時,它會一直持有指向目前類(B類)的常量池(方法區中的一個資料結構)的指針。
注意:虛拟機開始執行B類中main()方法的位元組碼的時候,盡管A類還沒被裝載,但是和大多數(也許所有)虛拟機實作一樣,它不會等到把程式中用到的所有類都裝載後才開始運作。恰好相反,它隻會需要時才裝載相應的類。
main()的第一條指令告知虛拟機為列在常量池第一項的類配置設定足夠的記憶體。是以虛拟機使用指向B常量池的指針找到第一項,發現它是一個對A類的符号引用,然後它就檢查方法區,看A類是否已經被加載了。
這個符号引用僅僅是一個給出了類A的全限定名“A”的字元串。為了能讓虛拟機盡可能快地從一個名稱找到類,虛拟機的設計者應當選擇最佳的資料結構和算法。
當虛拟機發現還沒有裝載過名為“A”的類時,它就開始查找并裝載檔案“A.class”,并把從讀入的二進制資料中提取的類型資訊放在方法區中。
緊接着,虛拟機以一個直接指向方法區A類資料的指針來替換常量池第一項(就是那個字元串“A”),以後就可以用這個指針來快速地通路A類了。這個替換過程稱為常量池解析,即把常量池中的符号引用替換為直接引用。
終于,虛拟機準備為一個新的A對象配置設定記憶體。此時它又需要方法區中的資訊。還記得剛剛放到B類常量池第一項的指針嗎?現在虛拟機用它來通路A類型資訊,找出其中記錄的這樣一條資訊:一個A對象需要配置設定多少堆空間。
JAVA虛拟機總能夠通過存儲與方法區的類型資訊來确定一個對象需要多少記憶體,當JAVA虛拟機确定了一個A對象的大小後,它就在堆上配置設定這麼大的空間,并把這個對象執行個體的變量speed初始化為預設初始值0。
當把新生成的A對象的引用壓到棧中,main()方法的第一條指令也完成了。接下來的指令通過這個引用調用Java代碼(該代碼把speed變量初始化為正确初始值5)。另一條指令将用這個引用調用Lava對象引用的run()方法。
2.4 堆
Java程式在運作時建立的所有類執行個體或數組都放在同一個堆中。而一個JAVA虛拟機執行個體中隻存在一個堆空間,是以所有線程都将共享這個堆。又由于一個Java程式獨占一個JAVA虛拟機執行個體,因而每個Java程式都有它自己的堆空間——它們不會彼此幹擾。但是同一個Java程式的多個線程卻共享着同一個堆空間,在這種情況下,就得考慮多線程通路對象(堆資料)的同步問題了。
JAVA虛拟機有一條在堆中配置設定新對象的指令,卻沒有釋放記憶體的指令,正如你無法用Java代碼區明确釋放一個對象一樣。虛拟機自己負責決定如何以及何時釋放不再被運作的程式引用的對象所占據的記憶體。通常,虛拟機把這個任務交給垃圾收集器。
2.4.1 數組的内部表示
在Java中,數組是真正的對象。和其他對象一樣,數組總是存儲在堆中。同樣,數組也擁有一個與它們的類相關聯的Class執行個體,所有具有相同次元和類型的數組都是同一個類的執行個體,而不管數組的長度(多元數組每一維的長度)是多少。例如一個包含3個int整數的數組和一個包含300個整數的數組擁有同一個類。數組的長度隻與執行個體資料有關。
數組類的名稱由兩部分組成:每一維用一個方括号“[”表示,用字元或字元串表示元素類型。比如,元素類型為int整數的、一維數組的類名為“[I”,元素類型為byte的三維數組為“[[[B”,元素類型為Object的二維數組為“[[Ljava/lang/Object”。
多元數組被表示為數組的數組。比如,int類型的二維數組,将表示為一個一維數組,其中的每一個元素是一個一維int數組的引用,如下圖:
在堆中的每個數組對象還必須儲存的資料時數組的長度、數組資料,以及某些指向數組的類資料的引用。虛拟機必須能夠通過一個數組對象的引用得到此數組的長度,通過索引通路其元素(期間要檢查數組邊界是否越界),調用所有數組的直接超類Object聲明的方法等等。
2.5 程式計數器
對于一個運作中的Java程式而言,其中的每一個線程都有它自己的PC(程式計數器)寄存器,它是在該線程啟動時建立的,PC寄存器的大小是一個字長,是以它既能夠持有一個本地指針,也能夠持有一個returnAddress。當線程執行某個Java方法時,PC寄存器的内容總是下一條将被執行指令的“位址”,這裡的“位址”可以是一個本地指針,也可以是在方法位元組碼中相對于該方法起始指令的偏移量。如果該線程正在執行一個本地方法,那麼此時PC寄存器的值是“undefined”。
2.6 Java棧
每當啟動一個新線程時,Java虛拟機都會為它配置設定一個Java棧。Java棧以幀為機關儲存線程的運作狀态。虛拟機隻會直接對Java棧執行兩種操作:以幀為機關的壓棧和出棧。
某個線程正在執行的方法被稱為該線程的目前方法,目前方法使用的棧幀稱為目前幀,目前方法所屬的類稱為目前類,目前類的常量池稱為目前常量池。線上程執行一個方法時,它會跟蹤目前類和目前常量池。此外,當虛拟機遇到棧内操作指令時,它對目前幀内資料執行操作。
每當線程調用一個Java方法時,虛拟機都會在該線程的Java棧中壓入一個新幀。而這個新幀自然就成為了目前幀。在執行這個方法時,它使用這個幀來存儲參數、局部變量、中間運算結果等資料。
Java方法可以以兩種方式完成。一種通過return傳回的,稱為正常傳回;一種是通過抛出異常而異常終止的。不管以哪種方式傳回,虛拟機都會将目前幀彈出Java棧然後釋放掉,這樣上一個方法的幀就成為目前幀了。
Java幀上的所有資料都是此線程私有的。任何線程都不能通路另一個線程的棧資料,是以我們不需要考慮多線程情況下棧資料的通路同步問題。當一個線程調用一個方法時,方法的的局部變量儲存在調用線程Java棧的幀中。隻有一個線程能總是通路那些局部變量,即調用方法的線程。
2.7 本地方法棧
前面提到的所有運作時資料區都是Java虛拟機規範中明确定義的,除此之外,對于一個運作中的Java程式而言,它還可能會用到一些跟本地方法相關的資料區。當某個線程調用一個本地方法時,它就進入了一個全新的并且不再受虛拟機限制的世界。本地方法可以通過本地方法接口來通路虛拟機的運作時資料區,但不止如此,它還可以做任何它想做的事情。
本地方法本質上時依賴于實作的,虛拟機實作的設計者們可以自由地決定使用怎樣的機制來讓Java程式調用本地方法。
任何本地方法接口都會使用某種本地方法棧。當線程調用Java方法時,虛拟機會建立一個新的棧幀并壓入Java棧。然而當它調用的是本地方法時,虛拟機會保持Java棧不變,不再線上程的Java棧中壓入新的幀,虛拟機隻是簡單地動态連接配接并直接調用指定的本地方法。
如果某個虛拟機實作的本地方法接口是使用C連接配接模型的話,那麼它的本地方法棧就是C棧。當C程式調用一個C函數時,其棧操作都是确定的。傳遞給該函數的參數以某個确定的順序壓入棧,它的傳回值也以确定的方式傳回調用者。同樣,這就是虛拟機實作中本地方法棧的行為。
很可能本地方法接口需要回調Java虛拟機中的Java方法,在這種情況下,該線程會儲存本地方法棧的狀态并進入到另一個Java棧。
下圖描繪了這樣一個情景,就是當一個線程調用一個本地方法時,本地方法又回調虛拟機中的另一個Java方法。這幅圖展示了JAVA虛拟機内部線程運作的全景圖。一個線程可能在整個生命周期中都執行Java方法,操作它的Java棧;或者它可能毫無障礙地在Java棧和本地方法棧之間跳轉。
該線程首先調用了兩個Java方法,而第二個Java方法又調用了一個本地方法,這樣導緻虛拟機使用了一個本地方法棧。假設這是一個C語言棧,其間有兩個C函數,第一個C函數被第二個Java方法當做本地方法調用,而這個C函數又調用了第二個C函數。之後第二個C函數又通過本地方法接口回調了一個Java方法(第三個Java方法),最終這個Java方法又調用了一個Java方法(它成為圖中的目前方法)。
轉載 http://www.cnblogs.com/java-my-life/archive/2012/08/01/2615221.html