前言
本來計劃要寫Android記憶體優化的,覺得有必要在此之前介紹一下Java虛擬機器的相關知識,Java虛擬機器也並不是三言兩語能夠介紹完的,因此開了Java虛擬機器系列,這一篇文章我們來學習Java虛擬機器的結構原理與執行時資料區域。
1.Java虛擬機器概述
Oracle官方定義的Java技術體系主要包括以下幾個部分:
- Java程式設計語言
- 各種平臺的Java虛擬機器
- Class檔案格式
- Java API類庫
- 第三方Java類庫
可以把Java程式設計語言、Java虛擬機器和Java API類庫這三部分統稱為JDK(Java Development Kit),它是Java程式開發的最小環境。另外,Java API中的Java SE API子集和Java虛擬機器這兩部分統稱為JRE(Java Runtime Environment),它是Java程式執行的標準環境。
從上面可以看出Java虛擬機器及其重要,它是整個Java平臺的基石,是Java語言編譯程式碼的執行平臺。你可以把Java虛擬機器看做一個抽象的計算機,它有各種指令集和各種執行時資料區域。
1.1 Java虛擬機器家族
很多同學可能認為Java虛擬機器,就是一個虛擬機器而已,它還有家族?或者認為Java虛擬機器指的就是Oracle的HotSpot虛擬機器。這裡來簡單介紹Java虛擬機器家族,自從1996年Sun公司釋出的JDK1.0中包含的Sun Classic VM到今天,出現和消亡了很多種虛擬機器,我們這裡只簡單介紹目前存活的相對主流Java虛擬機器。
HotSpot VM
Oracle JDK和OpenJDK中自帶的虛擬機器,是最主流的和使用範圍最廣的Java虛擬機器。介紹Java虛擬機器的技術文章,如果不做特殊說明,大部分都是介紹HotSpot VM的。HotSpot VM並非是Sun公司開發的,而是由Longview Technologies這家小公司設計的,它在1997年被Sun公司收購,Sun公司又在2009年被Oracle收購。
J9 VM
J9 VM 是IBM開發的VM,目前是其主力發展的Java虛擬機器。J9 VM的市場定位和HotSpot VM接近,它是一款設計上從服務端到桌面應用再到嵌入式都考慮到的多用途虛擬機器,目前J9 VM的效能水平大致跟HotSpot VM是一個檔次的。
Zing VM
以Oracle的HotSpot VM為基礎,改進了許多會影響延遲的細節。最大的三個賣點是:
- 1.低延遲,“無暫停”的C4 GC,GC帶來的暫停可以控制在10ms以下的級別,支援的Java堆大小可以到1TB;
- 2.啟動後快速預熱功能。
- 3.可管理性:零開銷、可在生產環境全時開啟的、整合在JVM內的監控工具Zing Vision。
1.2 Java虛擬機器執行流程
當我們執行一個Java程式時,它的執行流程是怎樣的呢?如下圖所示。
從上圖可以看到Java虛擬機器與java語言沒有什麼必然聯絡,它只與特定的二進位制檔案:Class檔案有關。
2.Java虛擬機器結構
這裡所講的體系結構,是指的Java虛擬機器的抽象行為,而不是具體的比如HotSpot VM的實現。按照Java虛擬機器規範,抽象的Java虛擬機器如下圖所示。
2.1 Class檔案格式
Java檔案被編譯後生成了Class檔案,這種二進位制格式檔案不依賴於特定的硬體和作業系統。每一個Class檔案中都對應著唯一一個類或者介面的的定義資訊,但是類或者介面並不一定定義在檔案中,比如類和介面可以通過類載入器來直接生成。
ClassFile的檔案結構如下所示。
ClassFile {
u4 magic; //魔數,固定值為0xCAFEBABE,用來判斷當前檔案是能被Java虛擬機器處理的Class檔案
u2 minor_version; //副版本號
u2 major_version; //主版本號
u2 constant_pool_count; //常量池計數器
cp_info constant_pool[constant_pool_count-1]; //常量池
u2 access_flags; //類和介面層次的訪問標誌
u2 this_class; //類索引
u2 super_class; //父類索引
u2 interfaces_count; //介面計數器
u2 interfaces[interfaces_count]; //介面表
u2 fields_count; //欄位計數器
field_info fields[fields_count]; //欄位表
u2 methods_count; //方法計數器
method_info methods[methods_count]; //方法表
u2 attributes_count; //屬性計數器
attribute_info attributes[attributes_count]; //屬性表
}複製程式碼
2.2 類載入器子系統
類載入器子系統通過多種類載入器來查詢和載入Class檔案到 Java 虛擬機器中。Java虛擬機器有兩種類載入器:系統載入器和使用者自定義載入器。其中系統載入器包括以下三種:
- 引導類載入器(Bootstrap Class Loader):用C/C++程式碼實現的載入器,用以載入Java虛擬機器執行時所需要的系統類,這些系統類在{JRE_HOME}/lib目錄下。Java虛擬機器的啟動就是通過引導類載入器建立一個初始類來完成的。由於類載入器是使用平臺相關的底層C/C++語言實現的, 所以該載入器不能被Java程式碼訪問到。但是,我們可以查詢某個類是否被引導類載入器載入過。引導類裝載器並不繼承java.lang.ClassLoader。
- 擴充套件類載入器(Extensions Class Loader):用於載入 Java 的擴充類 ,擴充類一般會放在 {JRE_HOME}/lib/ext/ 目錄下,用來提供除了系統類之外的額外功能。
- 應用程式類載入器(Application Class Loader):該類載入器是用於載入使用者程式碼,是使用者程式碼的入口。應用類載入器將擴充類載入器當成自己的父類載入器,當嘗試載入類的時候,首先嚐試讓擴充類載入器載入,如果擴充類載入器載入成功,則直接返回載入結果Class
instance,如果載入失敗,則會詢問引導類載入器是否已經載入了該類,如果沒有,應用類載入器才會嘗試自己載入。
使用者自定義載入器,則是通過繼承 java.lang.ClassLoader類的方式來實現自己的類載入器。
類載入器子系統除了要載入Class檔案類到 Java 虛擬機器中,還必須負責驗證被匯入的Class類的正確性,為類變數分配並初始化記憶體,以及幫助解析符號引用。這些動作必須嚴格按以下順序進行:
1.裝載:查詢並載入Class檔案。
2.連結:驗證、準備、以及解析。
- 驗證:確保被匯入型別的正確性。
- 準備:為類的靜態欄位分配欄位,並用預設值初始化這些欄位。
- 解析:根據執行時常量池的符號引用來動態決定具體值得過程。
3.初始化:將類變數初始化為正確初始值。
2.3 資料型別
Java虛擬機器與Java語言的資料型別相似,可以分為兩類:基本型別和引用型別。Java虛擬機器希望編譯器在編譯期間儘可能的完成型別檢查,使得虛擬機器在執行期間無需進行型別檢查操作。
2.4 執行時資料區域
很多人將Java的記憶體分為堆記憶體(heap)和棧記憶體(Stack),這種分發不夠準確,Java的記憶體區域劃分實際上遠比這複雜。
Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為不同的資料區域,根據《Java虛擬機器規範(Java SE7版)》的規定,這些資料區域分別為程式計數器、Java虛擬機器棧、本地方法棧、Java堆和方法區,下面我們來一一的對它們進行介紹。
2.4.1 程式計數器
為了保證程式能夠連續地執行下去,處理器必須具有某些手段來確定下一條指令的地址,而程式計數器正是起到這種作用。
程式計數器(Program Counter Register)也叫做PC暫存器,是一塊較小的記憶體空間。在虛擬機器概念模型中,位元組碼直譯器工作時就是通過改變程式計數器來選取下一條需要執行的位元組碼指令,Java虛擬機器的多執行緒是通過輪流切換並分配處理器執行時間的方式來實現的,在一個確定的時刻只有一個處理器執行一條執行緒中的指令,為了線上程切換後能恢復到正確的執行位置,每個執行緒都會有一個獨立的程式計數器,因此,程式計數器是執行緒私有的。如果執行緒執行的方法不是Native方法,則程式計數器儲存正在執行的位元組碼指令地址,如果是Native方法則程式計數器的值則為空(Undefined)。程式計數器是Java虛擬機器規範中唯一沒有規定任何OutOfMemoryError情況的資料區域。
2.4.2 Java虛擬機器棧
每一條Java虛擬機器執行緒都有一個執行緒私有的Java虛擬機器棧(Java Virtual Machine Stacks)。它的生命週期與執行緒相同,與執行緒是同時建立的。Java虛擬機器棧儲存執行緒中Java方法呼叫的狀態,包括區域性變數、引數、返回值以及運算的中間結果等。一個Java虛擬機器棧包含了多個棧幀,一個棧幀用來儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。當執行緒呼叫一個Java方法時,虛擬機器壓入一個新的棧幀到該執行緒的Java棧中,當該方法執行完成,這個棧幀就從Java棧中彈出。我們平常所說的棧記憶體(Stack)指的就是Java虛擬機器棧。
Java虛擬機器規範中定義了兩種異常情況:
- 如果執行緒請求分配的棧容量超過Java虛擬機器所允許的的最大容量,Java虛擬機器會丟擲StackOverflowError。
- 如果Java虛擬機器棧可以動態擴充套件(大部分Java虛擬機器都可以動態擴充套件),但是擴充套件時無法申請到足夠的記憶體,或者在建立新的執行緒時沒有足夠的記憶體去建立對應的Java虛擬機器棧,則會丟擲OutOfMemoryError異常。
2.4.3 本地方法棧
Java虛擬機器實現可能要用到C Stacks來支援Native語言,這個C Stacks就是本地方法棧(Native Method Stack)。它與Java虛擬機器棧類似,只不過本地方法棧是用來支援Native方法服務。如果Java虛擬機器不支援Native方法,並且也不依賴於C Stacks,可以無需支援本地方法棧。在Java虛擬機器規範中對本地方法棧的語言和資料結構等沒有強制規定,因此具體的Java虛擬機器可以自由實現它,比如HotSpot VM將本地方法棧和Java虛擬機器棧合二為一。
與Java虛擬機器棧類似,本地方法棧也會丟擲 StackOverflowError和OutOfMemoryError異常
2.4.4 Java堆
Java堆(Java Heap)是被所有執行緒共享的執行時記憶體區域。Java堆用來存放物件例項,幾乎所有的物件例項都在這裡分配記憶體。Java堆儲存的物件被垃圾收集器管理,這些受管理的物件無需也無法顯示的銷燬。從記憶體回收的角度,Java堆可以粗略的分為新生代和老年代。從記憶體分配的角度Java堆中可能劃分出多個執行緒私有的分配緩衝區。不管如何劃分,Java堆儲存的內容是不變的,進行劃分是為了能更快的回收或者分配記憶體。
Java堆的容量可以時固定的,也可以動態的擴充套件。Java堆的所使用的記憶體在物理上不需要連續,邏輯上連續即可。
Java虛擬機器規範中定義了一種異常情況:
- 如果在堆中沒有足夠的記憶體來完成例項分配,並且堆也無法進行擴充套件時,則會丟擲OutOfMemoryError異常。
2.4.5 方法區
方法區(Method Area)是被所有執行緒共享的執行時記憶體區域。用來儲存已經被Java虛擬機器載入的類的結構資訊,包括:
執行時常量池、欄位和方法資訊、靜態變數等資料。方法區是Java堆的邏輯組成部分,它一樣在物理上不需要連續,並且可以選擇在方法區中不實現垃圾收集。方法區並不等同於永久代,只是因為HotSpot VM使用永久代來實現方法區,對於其他的Java虛擬機器,比如J9和JRockit等,並不存在永久代概念。
Java虛擬機器規範中定義了一種異常情況:
- 如果方法區的記憶體空間不滿足記憶體分配需求時,Java虛擬機器會丟擲OutOfMemoryError異常。
執行時常量池
執行時常量池(Runtime Constant Pool)是方法區的一部分。在2.1 Class檔案格式這一小節中我們得知,Class檔案不僅包含了類的版本、介面、欄位和方法等資訊,還包含了常量池,它用來存放編譯時期生成的字面量和符號引用,這些內容會在類載入後存放在方法區的執行時常量池中。執行時常量池可以理解為是類或介面的常量池的執行時表現形式。
Java虛擬機器規範中定義了一種異常情況:
當建立類或介面時,如果構造執行時常量池所需的記憶體超過了方法區所能提供的最大值,Java虛擬機器會丟擲OutOfMemoryError異常。
參考資料
《深入理解Java虛擬機器 第二版》
《Java虛擬機器規範(Java SE7版)》
理解Java虛擬機器體系結構
目前主流的 Java 虛擬機器有哪些?-知乎
jvm執行時資料區域解析
Java虛擬機器原理圖解
深入探討 Java 類載入器