Java 虛拟機工作原理詳解
一、類加載器 首先來看一下 java 程式的執行過程。

從這個框圖很容易大體上了解 java 程式工作原理。首先,你寫好 java 代碼,儲存到硬碟當中。然後你在指令行中輸入
javac YourClassName.java
此時,你的 java 代碼就被編譯成位元組碼(.class).如果你是在 Eclipse IDE 或者其他開發工具中,你儲存代碼的時候,開發工具已經幫你完成了上述的編譯工作,是以你可以在對應的目錄下看到 class 檔案。此時的 class 檔案依然是儲存在硬碟中,是以,當你在指令行中運作
java YourClassName
就完成了上面紅色方框中的工作。JRE 的來加載器從硬碟中讀取 class 檔案,載入到系統配置設定給 JVM 的記憶體區域--運作資料區(Runtime Data Areas). 然後執行引擎解釋或者編譯類檔案,轉化成特定 CPU 的機器碼,CPU 執行機器碼,至此完成整個過程。
接下來就重點研究一下類加載器究竟為何物?又是如何工作的? 首先看一下來加載器的一些特點,有點抽象,不過總有幫助的。
》》層級結構 類加載器被組織成一種層級結構關系,也就是父子關系。其中,Bootstrap 是所有類加載器的父親。如下圖所示:
--Bootstrap class loader: 當運作 java 虛拟機時,這個類加載器被建立,它加載一些基本的 java API,包括 Object 這個類。需要注意的是,這個類加載器不是用 java 語言寫的,而是用 C/C++ 寫的。 --Extension class loader: 這個加載器加載出了基本 API 之外的一些拓展類,包括一些與安全性能相關的類。(目前了解得不是很深,隻能籠統說,待日後再詳細說明) --System Class Loader: 它加載應用程式中的類,也就是在你的 classpath 中配置的類。 --User-Defined Class Loader: 這是開發人員通過拓展 ClassLoader 類定義的自定義加載器,加載程式員定義的一些類。
》》委派模式(Delegation Mode) 仔細看上面的層次結構,當 JVM 加載一個類的時候,下層的加載器會将将任務委托給上一層類加載器,上一層加載檢查它的命名空間中是否已經加載這個類,如果已經加載,直接使用這個類。如果沒有加載,繼續往上委托直到頂部。檢查完了之後,按照相反的順序進行加載,如果 Bootstrap 加載器找不到這個類,則往下委托,直到找到類檔案。對于某個特定的類加載器來說,一個 Java 類隻能被載入一次,也就是說在 Java 虛拟機中,類的完整辨別是(classLoader,package,className)。一個雷可以被不同的類加載器加載。
舉個具體的例子來說明,現在加入我有一個自己定義的類 MyClass 需要加載,如果不指定的話,一般交 App(System)加載。接到任務後,System 檢查自己的庫裡是否已經有這個類,發現沒有之後委托給 Extension,Extension 進行同樣的檢查,發現還是沒有繼續往上委托,最頂層的 Boots 發現自己庫裡也沒有,于是根據它的路徑(Java 核心類庫,如 java.lang)嘗試去加載,沒找到這個 MaClass 類,于是隻好(人家看好你,交給你完成,你無能為力,隻好交給别人啦)往下委托給 Extension,Extension 到自己的路徑(JAVA_HOME/jre/lib/ext)是找,還是沒找到,繼續往下,此時 System 加載器到 classpath 路徑尋找,找到了,于是加載到 Java 虛拟機。 現在假設我們将這個類放到 JAVA_HOME/jre/lib/ext 這個路徑中去(相當于交給 Extension 加載器加載),按照同樣的規則,最後由 Extension 加載器加載 MyClass 類,看到了吧,統一各類被兩次加載到 JVM,但是每次都是由不同的 ClassLoader 完成。
》》可見性限制 下層的加載器能夠看到上層加載器中的類,反之則不行,也就是是說委托隻能從下到上。
》》不允許解除安裝類 類加載器可以加載一個類,但是它不能解除安裝一個類。但是類加載器可以被删除或者被建立。
當類加載完畢之後,JVM 繼續按照下圖完成其他工作:
框圖中各個步驟簡單介紹如下: Loading:文章前面介紹的類加載,将檔案系統中的 Class 檔案載入到 JVM 記憶體(運作資料區域) Verifying:檢查載入的類檔案是否符合 Java 規範和虛拟機規範。 Preparing:為這個類配置設定所需要的記憶體,确定這個類的屬性、方法等所需的資料結構。(Prepare a data structure that assigns the memory required by classes and indicates the fields, methods, and interfaces defined in the class.) Resolving:将該類常量池中的符号引用都改變為直接引用。(不是很了解) Initialing:初始化類的局部變量,為靜态域指派,同時執行靜态初始化塊。
那麼,Class Loader 在加載類的時候,究竟做了些什麼工作呢? 要了解這其中的細節,必須得先詳細介紹一下運作資料區域。
二、運作資料區域 Runtime Data Areas:當運作一個 JVM 示例時,系統将配置設定給它一塊記憶體區域(這塊記憶體區域的大小可以設定的),這一記憶體區域由 JVM 自己來管理。從這一塊記憶體中分出一塊用來存儲一些運作資料,例如建立的對象,傳遞給方法的參數,局部變量,傳回值等等。分出來的這一塊就稱為運作資料區域。運作資料區域可以劃分為6大塊:Java 棧、程式計數寄存器(PC 寄存器)、本地方法棧(Native Method Stack)、Java 堆、方法區域、運作常量池(Runtime Constant Pool)。運作常量池本應該屬于方法區,但是由于其重要性,JVM 規範将其獨立出來說明。其中,前面3各區域(PC 寄存器、Java 棧、本地方法棧)是每個線程獨自擁有的,後三者則是整個 JVM 執行個體中的所有線程共有的。這六大塊如下圖所示:
》PC 計數器: 每一個線程都擁有一個 PC 計數器,當線程啟動(start)時,PC 計數器被建立,這個計數器存放目前正在被執行的位元組碼指令(JVM 指令)的位址。 》Java 棧: 同樣的,Java 棧也是每個線程單獨擁有,線程啟動時建立。這個棧中存放着一系列的棧幀(Stack Frame),JVM 隻能進行壓入(Push)和彈出(Pop)棧幀這兩種操作。每當調用一個方法時,JVM 就往棧裡壓入一個棧幀,方法結束傳回時彈出棧幀。如果方法執行時出現異常,可以調用 printStackTrace 等方法來檢視棧的情況。棧的示意圖如下:
OK。現在我們再來詳細看看每一個棧幀中都放着什麼東西。從示意圖很容易看出,每個棧幀包含三個部分:本地變量數組,操作數棧,方法所屬類的常量池引用。 》局部(本地)變量數組: 局部(本地)變量數組中,從0開始按順序存放方法所屬對象的引用、傳遞給方法的參數、局部變量。舉個例子:
public void doSomething(int a, double b, Object o) {
...
}
這個方法的棧幀中的局部變量存儲的内容分别是:
0: this
1: a
2,3:b
4:0
看仔細了,其中 double 類型的 b 需要兩個連續的索引。取值的時候,取出的是2這個索引中的值。如果是靜态方法,則數組第0個不存放 this 引用,而是直接存儲傳遞的參數。 》操作數棧: 操作數棧中存放方法執行時的一些中間變量,JVM 在執行方法時壓入或者彈出這些變量。其實,操作數棧是方法真正工作的地方,執行方法時,局部變量數組與操作數棧根據方法定義進行資料交換。例如,執行以下代碼時,操作數棧的情況如下:
int a = 90;
int b = 10;
int c = a + b;
注意在這個圖中,操作數棧的地步是在上邊,是以先壓入的100位于上方。可以看出,操作數棧其實是一個資料臨時存儲區,存放一些中間變量,方法結束了,操作數棧也就沒有啦。 》棧幀中資料引用: 除了局部變量數組和操作數棧之外,棧幀還需要一個常量池的引用。當 JVM 執行到需要常量池的資料時,就是通過這個引用來通路常量池的。棧幀中的資料還要負責處理方法的傳回和異常。如果通過 return 傳回,則将該方法的棧幀從 Java 棧中彈出。如果方法有傳回值,則将傳回值壓入到調用該方法的方法的操作數棧中。另外,資料區中還儲存中該方法可能的異常表的引用。下面的例子用來說明:
class Example3C{
public static void addAndPrint(){
double result = addTwoTypes(1,88.88);
System.out.println(result);
}
public static double addTwoTypes(int i, double d){
return i+d;
}
}
執行上述代碼時,Java 棧如下圖所示:
花些時間好好研究上圖。一樣需要注意的是,棧的底部在上方,先押人員 addAndPrint 方法的棧幀,再壓入 addTwoTypes 方法的棧幀。上圖最右邊的文字說明有錯誤,應該是 addTwoTypes 的執行結果存放在 addAndPrint 的操作數棧中。 》》本地方法棧 當程式通過 JNI(Java Native Interface)調用本地方法(如 C 或者 C++ 代碼)時,就根據本地方法的語言類型建立相應的棧。 》》方法區域 方法區域是一個 JVM 執行個體中的所有線程共享的,當啟動一個 JVM 執行個體時,方法區域被建立。它用于存運作放常量池、有關域和方法的資訊、靜态變量、類和方法的位元組碼。不同的 JVM 實作方式在實作方法區域的時候會有所差別。Oracle 的 HotSpot 稱之為永久區域(Permanent Area)或者永久代(Permanent Generation)。 》》運作常量池 這個區域存放類和接口的常量,除此之外,它還存放方法和域的所有引用。當一個方法或者域被引用的時候,JVM 就通過運作常量池中的這些引用來查找方法和域在記憶體中的的實際位址。 》》堆(Heap) 堆中存放的是程式建立的對象或者執行個體。這個區域對 JVM 的性能影響很大。垃圾回收機制處理的正是這一塊記憶體區域。 是以,類加載器加載其實就是根據編譯後的 Class 檔案,将 java 位元組碼載入 JVM 記憶體,并完成對運作資料處于的初始化工作,供執行引擎執行。
三、 執行引擎(Execution Engine) 類加載器将位元組碼載入記憶體之後,執行引擎以 Java 位元組碼指令為但願,讀取 Java 位元組碼。問題是,現在的 java 位元組碼機器是讀不懂的,是以還必須想辦法将位元組碼轉化成平台相關的機器碼。這個過程可以由解釋器來執行,也可以有即時編譯器(JIT Compiler)來完成。
熬夜不易,點選請老王喝杯烈酒!!!!!!!