一個執行階段的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虛擬機器運行一個程式時,它需要記憶體來儲存許多東西,例如:字節碼、從已裝載的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中参数的传递有两种,分别是按值传递和按引用传递。按值传递不必多说,下面就说一下按引用传递。
“当一个对象被当作参数传递到一个方法”,这就是所谓的按引用传递。
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"); } public static void main(String[] args) { Test test = new Test(); User user = new User(); test.set(user); System.out.println(user.getName()); } }
上面代码的输出结果是“hello world”,这不必多说,那如果将set方法改为如下,结果会是多少呢?
public void set(User user){ user.setName("hello world"); user = new User(); user.setName("change"); }
答案依然是“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"。如下图:
在JAVA虛擬機器中,負責尋找並裝載類型的那部分稱為類別裝載子系統。
JAVA虛擬機器有兩種類別裝載器:啟動類別裝載器和使用者自訂類別裝載器。前者是JAVA虛擬機器實作的一部分,後者則是Java程式的一部分。由不同的類別裝載器裝載的類別將被放在虛擬機器內部的不同命名空間中。
類別裝載器子系統涉及Java虛擬機器的其他幾個組成部分,以及幾個來自java.lang程式庫的類別。例如,使用者自訂的類別裝載器是普通的Java對象,它的類別必須派生自java.lang.ClassLoader類別。 ClassLoader中定義的方法為程式提供了存取類別裝載器機制的介面。此外,對於每一個被裝載的類型,JAVA虛擬機器都會為它建立一個java.lang.Class類別的實例來代表該類型。和所有其他物件一樣,使用者自訂的類別裝載器以及Class類別的實例都放在記憶體中的堆區,而裝載的類型資訊則都位於方法區。
類別裝載器子系統除了要定位和導入二進制class檔案外,還必須負責驗證被導入類別的正確性,為類別變數分配並初始化內存,以及幫助解析符號引用。這些動作必須嚴格依照以下順序進行:
● 驗證 確保被導入類型的正確性。
● 準備 為類別變數分配內存,並將其初始化為預設值。
● 解析 把類型中的符號參考轉換為直接引用。
每個JAVA虛擬機器實作都必須有一個啟動類別裝載器,它知道怎麼裝載受信任的類別。
每個類別裝載器都有自己的命名空間,其中維護著由它裝載的類型。所以一個Java程式可以多次裝載具有同一個全限定名的多個型別。這樣一個類型的全限定名就不足以確定在一個Java虛擬機器中的唯一性。因此,當多個類別裝載器都裝載了同名的類型時,為了惟一地標識該類型,還要在類型名稱前加上裝載該類型(指出它所位於的命名空間)的類別裝載器標識。
在Java虛擬機器中,關於被裝載類型的資訊儲存在一個邏輯上被稱為方法區的記憶體中。當虛擬機器裝載某個類型時,它使用類別裝載器定位對應的class文件,然後讀入這個class文件-1個線性二進位資料流,然後它傳輸到虛擬機器中,緊接著虛擬機器提取其中的類型訊息,並將這些資訊儲存到方法區。此類型中的類別(靜態)變數同樣也儲存在方法區中。
JAVA虛擬機在內部如何儲存類型訊息,這是由具體實現的設計者來決定的。
當虛擬機器執行Java程式時,它會尋找使用儲存在方法區中的類型資訊。由於所有執行緒都共用方法區,因此它們對方法區資料的存取必須被設計為是執行緒安全的。例如,假設同時有兩個線程都企圖訪問一個名為Lava的類,而這個類還沒有被裝入虛擬機,那麼,這時只應該有一個線程去裝載它,而另一個線程則只能等待。
對於每個裝載的類型,虛擬機器都會在方法區中儲存以下類型資訊:
● 這個類型的全限定名稱
● 這個類型的直接超類別的全限定名稱(除非這個類型是java.lang.Object,它沒有超類別)
● 這個類型是類別類型還是介面類型
● 這個類型的存取修飾符(public、abstract或final的某個子集)
## ● 任何直接超介面的全數限定名的有序清單
除了上面列出的基本類型資訊外,虛擬機器還得為每個被裝載的類型儲存以下資訊:
●此類型的常數池● 字段資訊
## ● 方法資訊
对于类型中声明的每一个字段。方法区中必须保存下面的信息。除此之外,这些字段在类或者接口中的声明顺序也必须保存。
○ 字段名
○ 字段的类型
○ 字段的修饰符(public、private、protected、static、final、volatile、transient的某个子集)
对于类型中声明的每一个方法,方法区中必须保存下面的信息。和字段一样,这些方法在类或者接口中的声明顺序也必须保存。
○ 方法名
○ 方法的返回类型(或void)
○ 方法参数的数量和类型(按声明顺序)
○ 方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)
除了上面清单中列出的条目之外,如果某个方法不是抽象的和本地的,它还必须保存下列信息:
○ 方法的字节码(bytecodes)
○ 操作数栈和该方法的栈帧中的局部变量区的大小
○ 异常表
类变量是由所有类实例共享的,但是即使没有任何类实例,它也可以被访问。这些变量只与类有关——而非类的实例,因此它们总是作为类型信息的一部分而存储在方法区。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。
而编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量)则和一般的类变量处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。作为常量池或字节码流的一部分,编译时常量保存在方法区中——就和一般的类变量一样。但是当一般的类变量作为声明它们的类型的一部分数据面保存的时候,编译时常量作为使用它们的类型的一部分而保存。
每个类型被装载的时候,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的。如果是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。这是作为方法表中的类型数据的一部分保存的。
虚拟机会在动态连接期间使用这个信息。当某个类型引用另一个类型的时候,虚拟机会请求装载发起引用类型的类装载器来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的方式也是至关重要的。为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的。
对于每一个被装载的类型(不管是类还是接口),虚拟机都会相应地为它创建一个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对象。
为了展示虚拟机如何使用方法区中的信息,下面来举例说明:
class Lava {private int speed = 5;void flow(){ } }
public class Volcano { public static void main(String[] args){ Lava lava = new Lava(); lava.flow(); } }
不同的虚拟机实现可能会用完全不同的方法来操作,下面描述的只是其中一种可能——但并不是仅有的一种。
要運行Volcano程序,首先得以某種「依賴實現的」方式告訴虛擬機器「Volcano」這個名字。之後,虛擬機器將找到並讀入對應的class檔案“Volcano.class”,然後它會從匯入的class檔案裡的二進位資料中提取類型資訊並將其放到方法區中。透過執行儲存在方法區中的字節碼,虛擬機器開始執行main()方法,在執行時,它會一直持有指向當前類別(Volcano類別)的常數池(方法區中的一個資料結構)的指針。
注意:虛擬機器開始執行Volcano類別中main()方法的字節碼的時候,儘管Lava類別還沒被裝載,但是和大多數(也許所有)虛擬機器實作一樣,它不會等到把程式中用到的所有類別都裝載後才開始運作。剛好相反,它只會需要時才裝載相應的類別。
main()的第一條指令告知虛擬機器為列在常數池第一項的類別分配足夠的記憶體。所以虛擬機器使用指向Volcano常數池的指標來找出第一項,發現它是一個對Lava類別的符號引用,然後它就檢查方法區,看Lava類別是否已經被載入了。
這個符號引用只是一個給出了類別Lava的全限定名「Lava」的字串。為了能讓虛擬機器盡可能快速地從一個名稱找到類,虛擬機器的設計者應選擇最佳的資料結構和演算法。
當虛擬機器發現還沒有裝載過名為“Lava”的類別時,它就開始查找並裝載檔案“Lava.class”,並將從讀入的二進位資料中提取的類型資訊放在方法區中。
緊接著,虛擬機器以一個直接指向方法區Lava類別資料的指標來替換常數池第一項(就是那個字串「Lava」),以後就可以用這個指標來快速地存取Lava類了。這個替換過程稱為常數池解析,即將常數池中的符號引用替換為直接引用。
終於,虛擬機器準備為一個新的Lava物件分配記憶體。此時它又需要方法區的資訊。還記得剛放到Volcano類別常數池第一項的指標嗎?現在虛擬機器用它來存取Lava類型訊息,找出其中記錄的這樣一條訊息:一個Lava物件需要分配多少堆空間。
JAVA虛擬機總能夠透過儲存與方法區的類型資訊來確定一個物件需要多少內存,當JAVA虛擬機確定了一個Lava物件的大小後,它就在堆上分配這麼大的空間,並把這個物件實例的變數speed初始化為預設初始值0。
當把新產生的Lava物件的參考壓到堆疊中,main()方法的第一條指令也完成了。接下來的指令透過這個引用呼叫Java程式碼(程式碼把speed變數初始化為正確初始值5)。另一條指令將會用這個引用呼叫Lava物件所引用的flow()方法。
Java程式在執行時所建立的所有類別實例或陣列都放在同一個堆疊中。而一個JAVA虛擬機器實例中只存在一個堆空間,因此所有執行緒都會共用這個堆。又由於一個Java程式獨佔一個JAVA虛擬機器實例,因而每個Java程式都有它自己的堆疊空間-它們不會彼此幹擾。但是同一個Java程式的多個執行緒卻共享著同一個堆空間,在這種情況下,就得考慮多執行緒存取物件(堆資料)的同步問題了。
JAVA虛擬機器有一條在堆中分配新物件的指令,卻沒有釋放記憶體的指令,正如你無法用Java程式碼區明確釋放一個物件。虛擬機器自己負責決定如何以及何時釋放不再被運行的程式引用的物件所佔據的記憶體。通常,虛擬機器會把這個任務交給垃圾收集器。
在Java中,陣列是真正的物件。和其他物件一樣,陣列總是儲存在堆中。同樣,數組也擁有一個與它們的類別相關聯的Class實例,所有具有相同維度和類型的數組都是同一個類別的實例,而不管數組的長度(多維數組每維度的長度)是多少。例如一個包含3個int整數的陣列和一個包含300個整數的陣列擁有同一個類別。數組的長度只與實例資料有關。
陣列類別的名稱由兩部分組成:每一維以一個方括號「[」表示,用字元或字串表示元素類型。例如,元素類型為int整數的、一維數組的類別名為“[I”,元素類型為byte的三維數組為“[[[B”,元素類型為Object的二維數組為“[[Ljava/ lang/Object」。
多維數組被表示為陣列的陣列。例如,int型別的二維數組,將表示為一維數組,其中的每一個元素都是一維int數組的引用,如下圖:
#
在堆中的每個數組物件還必須保存的數據時數組的長度、數組數據,以及某些指向數組的類別數據的引用。虛擬機器必須能夠透過一個陣列物件的參考得到此陣列的長度,透過索引存取其元素(期間要檢查陣列邊界是否越界),呼叫所有陣列的直接超類別Object聲明的方法等等。
對於一個運行中的Java程式而言,其中的每個執行緒都有它自己的PC(程式計數器)暫存器,它是在該執行緒啟動時創建的,PC暫存器的大小是一個字長,因此它既能夠持有一個本地指針,也能夠持有一個returnAddress。當執行緒執行某個Java方法時,PC暫存器的內容總是下一條將被執行指令的“位址”,這裡的“位址”可以是一個本地指針,也可以是在方法字節碼中相對於該方法起始指令的偏移量。如果該執行緒正在執行一個本機方法,那麼此時PC暫存器的值是「undefined」。
每當啟動一個新執行緒時,Java虛擬機器都會為它指派一個Java棧。 Java堆疊以幀為單位保存執行緒的運行狀態。虛擬機器只會直接對Java棧執行兩種操作:以幀為單位的壓棧和出棧。
某個執行緒正在執行的方法稱為該執行緒的目前方法,目前方法使用的堆疊幀稱為目前幀,目前方法所屬的類別稱為目前類,目前類的常數池稱為當前常量池。在執行緒執行一個方法時,它會追蹤當前類別和當前常數池。此外,當虛擬機器遇到堆疊內操作指令時,它對目前幀內資料執行操作。
每當執行緒呼叫一個Java方法時,虛擬機器都會在該執行緒的Java棧中壓入一個新幀。而這個新幀自然就成為了當前幀。在執行這個方法時,它使用這個幀來儲存參數、局部變數、中間運算結果等資料。
Java方法可以用兩種方式完成。一種透過return返回的,稱為正常返回;一種是透過拋出異常而異常終止的。不管以哪種方式返回,虛擬機器都會將當前幀彈出Java堆疊然後釋放掉,這樣上一個方法的幀就成為當前幀了。
Java幀上的所有資料都是此執行緒私有的。任何執行緒都不能存取另一個執行緒的棧數據,因此我們不需要考慮多執行緒情況下棧資料的存取同步問題。當一個執行緒呼叫一個方法時,方法的局部變數會保存在呼叫執行緒Java棧的幀中。只有一個執行緒能總是存取那些局部變量,也就是呼叫方法的執行緒。
前面提到的所有執行時間資料區都是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方法(它成為圖中的目前方法)。
學習Java的同學注意了! ! !
學習過程中遇到什麼問題或想獲取學習資源的話,歡迎加入Java學習交流群:299541275 我們一起學習Java!
以上是Java虛擬機器體系結構的詳細介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!