一、前言
為什麼要學習瞭解Java虛擬機器
1.我們需要更加清楚的瞭解Java底層是如何運作的,有利於我們更深刻的學習好Java。
2.對我們除錯錯誤提供很寶貴的經驗。
3.這是合格的Java程式必須要了解的內容。
基於此,筆者打算出一個Java虛擬機器的系列,加深自己對知識點的理解,同時也方便各位有需要的園友。
二、Java虛擬機器的定義
Java虛擬機器(Java Virtual Machine),簡稱JVM。當我們說起Java虛擬機器時,可能指的是如下三種不同的東西:
1.抽象規範。
2.一個具體的實現。
3.一個執行中的虛擬機器例項。
Java虛擬機器抽象規範僅僅是一個概念,在《The Java Virtual Machine Specification》中有詳細的描述。該規範的實現,可能來自多個提供商,並存在於多個平臺上,它或者是全部由軟體實現,或者是以硬體和軟體相結合的方式來實現。當執行一個Java程式的時候,也就在執行一個Java虛擬機器例項。注意,我們所說的Java平臺無關性是指class檔案的平臺無關性,JVM是和平臺相關的,不同作業系統對應不同的JVM。
三、Java虛擬機器的總體框架圖
下圖是整個Java虛擬機器的總體框架圖,之後我們會經常涉及到。
四、Java虛擬機器的體系結構
下圖表示了Java虛擬機器的結構框圖,主要描述了JVM子系統和記憶體區。
五、Java虛擬機器各組成部分
5.1 類裝載子系統
類裝載子系統負責查詢並裝載型別,Java虛擬機器由兩種類裝載器:啟動類裝載器(Java虛擬機器實現的一部分)和使用者自定義類裝載器(Java程式的一部分)。類裝載子系統負責定位和匯入二進位制class檔案,並且保證匯入類的正確性,為類變數分配並初始化記憶體,以及幫助解析符號引用。類裝載器必須嚴格按照如下順序進行類的裝載。
1) 裝載 -- 查詢並裝載型別的二進位制資料
2) 連線 -- 執行驗證,準備,以及解析(可選),連線分為如下三個步驟
驗證 -- 確保被匯入型別的正確性
準備 -- 為類變數分配記憶體,並將其初始化為預設值
解析 -- 把型別中的符號引用轉換為直接引用
3) 初始化 -- 把類變數初始化為正確初始值
啟動類裝載器 -- Java虛擬機器必須有一個啟動類裝載器,用於裝載受信任的類,如Java API的class檔案。
使用者自定義類裝載器 -- 繼承自ClassLoader類,ClassLoader的如下四個方法,是通往Java虛擬機器的通道。
1. protected final Class defineClass(String name, byte data[], int offset, int length);
2. protected final Class defineClass(String name, byte data[], int offset, int length, ProtectionDomain protectionDomain);
3. protected final Class findSystemClass(String name);
4. protected final void resolveClass(Class c);
這四個方法涉及到了裝載和連線兩個階段,defineClass方法data引數為二進位制Java Class檔案格式,表示一個新的可用型別,之後把這個型別匯入到方法區中。findSystemClass的引數為全限定名,通過類裝載器進行裝載。resolveClass引數為Class例項,完成連線初始化操作。
5.2 方法區
方法區是執行緒共享的記憶體區域,用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料,當方法區無法滿足記憶體分配需求時,將丟擲OutOfMemoryError。
類資訊包括1.型別全限定名。2.型別的直接超類的全限定名(除非這個型別是java.lang.Object,它沒有超類)。3.型別是類型別還是介面型別。4.型別的訪問修飾符(public、abstract、final的某個子集)。5.任何直接超介面的全限定名的有序列表。6.型別的常量池。7.欄位資訊。8.方法資訊。9.除了常量以外的所有類(靜態)變數。10.一個到類ClassLoader的引用。11.一個到Class類的引用。
著重介紹常量池 -- 虛擬機器必須要為每個被裝載的型別維護一個常量池。常量池就是該型別所用常量的一個有序集合,包括直接常量和對其他型別、欄位和方法的符號引用。它在Java程式的動態連線中起著核心作用。
5.3 堆
一個虛擬機器例項只對應一個堆空間,堆是執行緒共享的。堆空間是存放物件例項的地方,幾乎所有物件例項都在這裡分配。堆也是垃圾收集器管理的主要區域。堆可以處於物理上不連續的記憶體空間中,只要邏輯上相連就行,當堆中沒有足夠的記憶體進行物件例項分配時,並且堆也無法擴充套件時,會丟擲OutOfMemoryError異常。
5.4 程式計數器
每個執行緒擁有自己的程式計數器,執行緒之間的程式計數器互不影響。PC暫存器的內容總是下一條將被執行指令的"地址",這裡的"地址"可以是一個本地指標,也可以是在方法位元組碼中相對於該方法起始指令的偏移量。如果該執行緒正在執行一個本地方法,則程式計數器內容為undefined,此區域在Java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。
5.5 Java棧
Java棧也是執行緒私有的,虛擬機器只會對棧進行兩種操作,以幀為單位的入棧和出棧。每個方法在執行時都會建立一個幀,併入棧,成為當前幀。棧幀由三部分組成:區域性變數區、運算元棧、幀資料區。
區域性變數區被組織為一個以字長為單位、從0開始計數的陣列。位元組碼指令通過從0開始的索引來使用其中的資料。型別為int、float、reference和return Address的值在陣列中只佔據一項,而型別為byte、short和char的值存入時都會轉化為int型別,也佔一項,而long、double則連續佔據兩項。
關於區域性變數區給出如下一個例子。
public class Example { public static int classMethod(int i, long l, float f, double d, Object o, byte b) { return 0; } public int instanceMethod(char c, double d, short s, boolean b) { return 0; } }
可以看到類方法的首項中沒有隱含的this指標,而物件方法則會隱含this指標。並且byte,char,short,boolean型別存入區域性變數區的時候都會被轉化成int型別值,當被存回堆或者方法區時,才會轉化回原來的型別。
運算元棧被組織成一個以字長為單位的陣列,它是通過標準的棧操作-入棧和出棧來進行訪問,而不是通過索引訪問。入棧和出棧也會存在型別的轉化。
棧資料區存放一些用於支援常量池解析、正常方法返回以及異常派發機制的資訊。即將常量池的符號引用轉化為直接地址引用、恢復發起呼叫的方法的幀進行正常返回,發生異常時轉交異常表進行處理。
5.6 本地方法棧
訪問本地方式時使用到的棧,為本地方法服務,本地方法區域也會丟擲StackOverflowError和OutOfMemoryError異常。
5.7 執行引擎
使用者所編寫的程式如何表現正確的行為需要執行引擎的支援,執行引擎執行位元組碼指令,完成程式的功能。後面會詳細介紹。
5.8 本地方法介面
本地方法介面稱為JNI,是為可移植性準備的。
六、總結
至此,虛擬機器的結構就已經大體介紹完了,現在我們只需要有一個初步的瞭解,後面對各個部分會有更加詳細的介紹。謝謝各位園友觀看~