鍍金池/ 教程/ Java/ Java 虛擬機
Struts2
Java 泛型
排序算法
Java 內(nèi)存管理
Webservice
Spring
輸入輸出流
Socket
字符串與數(shù)組
面向?qū)ο缶幊?/span>
海量數(shù)據(jù)處理
Hibernate
Netty
基本類型與運算符
常見設(shè)計模式
Java 虛擬機
Java 多線程
JDBC
搭建 Java 開發(fā)環(huán)境
Java 數(shù)據(jù)庫操作
異常處理
集合類
Servlet 與 JSP

Java 虛擬機

Java 虛擬機工作原理詳解

一、類加載器 首先來看一下 java 程序的執(zhí)行過程。

http://wiki.jikexueyuan.com/project/java-special-topic/images/30.png" alt="" />

從這個框圖很容易大體上了解 java 程序工作原理。首先,你寫好 java 代碼,保存到硬盤當中。然后你在命令行中輸入

javac YourClassName.java

此時,你的 java 代碼就被編譯成字節(jié)碼(.class).如果你是在 Eclipse IDE 或者其他開發(fā)工具中,你保存代碼的時候,開發(fā)工具已經(jīng)幫你完成了上述的編譯工作,因此你可以在對應(yīng)的目錄下看到 class 文件。此時的 class 文件依然是保存在硬盤中,因此,當你在命令行中運行

java YourClassName

就完成了上面紅色方框中的工作。JRE 的來加載器從硬盤中讀取 class 文件,載入到系統(tǒng)分配給 JVM 的內(nèi)存區(qū)域--運行數(shù)據(jù)區(qū)(Runtime Data Areas). 然后執(zhí)行引擎解釋或者編譯類文件,轉(zhuǎn)化成特定 CPU 的機器碼,CPU 執(zhí)行機器碼,至此完成整個過程。

接下來就重點研究一下類加載器究竟為何物?又是如何工作的? 首先看一下來加載器的一些特點,有點抽象,不過總有幫助的。

》》層級結(jié)構(gòu) 類加載器被組織成一種層級結(jié)構(gòu)關(guān)系,也就是父子關(guān)系。其中,Bootstrap 是所有類加載器的父親。如下圖所示:

http://wiki.jikexueyuan.com/project/java-special-topic/images/31.png" alt="" />

--Bootstrap class loader: 當運行 java 虛擬機時,這個類加載器被創(chuàng)建,它加載一些基本的 java API,包括 Object 這個類。需要注意的是,這個類加載器不是用 java 語言寫的,而是用 C/C++ 寫的。 --Extension class loader: 這個加載器加載出了基本 API 之外的一些拓展類,包括一些與安全性能相關(guān)的類。(目前了解得不是很深,只能籠統(tǒng)說,待日后再詳細說明) --System Class Loader: 它加載應(yīng)用程序中的類,也就是在你的 classpath 中配置的類。 --User-Defined Class Loader: 這是開發(fā)人員通過拓展 ClassLoader 類定義的自定義加載器,加載程序員定義的一些類。

》》委派模式(Delegation Mode) 仔細看上面的層次結(jié)構(gòu),當 JVM 加載一個類的時候,下層的加載器會將將任務(wù)委托給上一層類加載器,上一層加載檢查它的命名空間中是否已經(jīng)加載這個類,如果已經(jīng)加載,直接使用這個類。如果沒有加載,繼續(xù)往上委托直到頂部。檢查完了之后,按照相反的順序進行加載,如果 Bootstrap 加載器找不到這個類,則往下委托,直到找到類文件。對于某個特定的類加載器來說,一個 Java 類只能被載入一次,也就是說在 Java 虛擬機中,類的完整標識是(classLoader,package,className)。一個雷可以被不同的類加載器加載。

http://wiki.jikexueyuan.com/project/java-special-topic/images/32.png" alt="" />

舉個具體的例子來說明,現(xiàn)在加入我有一個自己定義的類 MyClass 需要加載,如果不指定的話,一般交 App(System)加載。接到任務(wù)后,System 檢查自己的庫里是否已經(jīng)有這個類,發(fā)現(xiàn)沒有之后委托給 Extension,Extension 進行同樣的檢查,發(fā)現(xiàn)還是沒有繼續(xù)往上委托,最頂層的 Boots 發(fā)現(xiàn)自己庫里也沒有,于是根據(jù)它的路徑(Java 核心類庫,如 java.lang)嘗試去加載,沒找到這個 MaClass 類,于是只好(人家看好你,交給你完成,你無能為力,只好交給別人啦)往下委托給 Extension,Extension 到自己的路徑(JAVA_HOME/jre/lib/ext)是找,還是沒找到,繼續(xù)往下,此時 System 加載器到 classpath 路徑尋找,找到了,于是加載到 Java 虛擬機。 現(xiàn)在假設(shè)我們將這個類放到 JAVA_HOME/jre/lib/ext 這個路徑中去(相當于交給 Extension 加載器加載),按照同樣的規(guī)則,最后由 Extension 加載器加載 MyClass 類,看到了吧,統(tǒng)一各類被兩次加載到 JVM,但是每次都是由不同的 ClassLoader 完成。

》》可見性限制 下層的加載器能夠看到上層加載器中的類,反之則不行,也就是是說委托只能從下到上。

》》不允許卸載類 類加載器可以加載一個類,但是它不能卸載一個類。但是類加載器可以被刪除或者被創(chuàng)建。

當類加載完畢之后,JVM 繼續(xù)按照下圖完成其他工作:

http://wiki.jikexueyuan.com/project/java-special-topic/images/33.png" alt="" />

框圖中各個步驟簡單介紹如下: Loading:文章前面介紹的類加載,將文件系統(tǒng)中的 Class 文件載入到 JVM 內(nèi)存(運行數(shù)據(jù)區(qū)域) Verifying:檢查載入的類文件是否符合 Java 規(guī)范和虛擬機規(guī)范。 Preparing:為這個類分配所需要的內(nèi)存,確定這個類的屬性、方法等所需的數(shù)據(jù)結(jié)構(gòu)。(Prepare a data structure that assigns the memory required by classes and indicates the fields, methods, and interfaces defined in the class.) Resolving:將該類常量池中的符號引用都改變?yōu)橹苯右谩#ú皇呛芾斫猓?Initialing:初始化類的局部變量,為靜態(tài)域賦值,同時執(zhí)行靜態(tài)初始化塊。

那么,Class Loader 在加載類的時候,究竟做了些什么工作呢? 要了解這其中的細節(jié),必須得先詳細介紹一下運行數(shù)據(jù)區(qū)域。

二、運行數(shù)據(jù)區(qū)域 Runtime Data Areas:當運行一個 JVM 示例時,系統(tǒng)將分配給它一塊內(nèi)存區(qū)域(這塊內(nèi)存區(qū)域的大小可以設(shè)置的),這一內(nèi)存區(qū)域由 JVM 自己來管理。從這一塊內(nèi)存中分出一塊用來存儲一些運行數(shù)據(jù),例如創(chuàng)建的對象,傳遞給方法的參數(shù),局部變量,返回值等等。分出來的這一塊就稱為運行數(shù)據(jù)區(qū)域。運行數(shù)據(jù)區(qū)域可以劃分為6大塊:Java 棧、程序計數(shù)寄存器(PC 寄存器)、本地方法棧(Native Method Stack)、Java 堆、方法區(qū)域、運行常量池(Runtime Constant Pool)。運行常量池本應(yīng)該屬于方法區(qū),但是由于其重要性,JVM 規(guī)范將其獨立出來說明。其中,前面3各區(qū)域(PC 寄存器、Java 棧、本地方法棧)是每個線程獨自擁有的,后三者則是整個 JVM 實例中的所有線程共有的。這六大塊如下圖所示:

http://wiki.jikexueyuan.com/project/java-special-topic/images/34.png" alt="" />

》PC 計數(shù)器: 每一個線程都擁有一個 PC 計數(shù)器,當線程啟動(start)時,PC 計數(shù)器被創(chuàng)建,這個計數(shù)器存放當前正在被執(zhí)行的字節(jié)碼指令(JVM 指令)的地址。 》Java 棧: 同樣的,Java 棧也是每個線程單獨擁有,線程啟動時創(chuàng)建。這個棧中存放著一系列的棧幀(Stack Frame),JVM 只能進行壓入(Push)和彈出(Pop)棧幀這兩種操作。每當調(diào)用一個方法時,JVM 就往棧里壓入一個棧幀,方法結(jié)束返回時彈出棧幀。如果方法執(zhí)行時出現(xiàn)異常,可以調(diào)用 printStackTrace 等方法來查看棧的情況。棧的示意圖如下:

http://wiki.jikexueyuan.com/project/java-special-topic/images/35.png" alt="" />

OK。現(xiàn)在我們再來詳細看看每一個棧幀中都放著什么東西。從示意圖很容易看出,每個棧幀包含三個部分:本地變量數(shù)組,操作數(shù)棧,方法所屬類的常量池引用。 》局部(本地)變量數(shù)組: 局部(本地)變量數(shù)組中,從0開始按順序存放方法所屬對象的引用、傳遞給方法的參數(shù)、局部變量。舉個例子:

public void doSomething(int a, double b, Object o) {
...
}

這個方法的棧幀中的局部變量存儲的內(nèi)容分別是:

0: this
1: a
2,3:b
4:0

看仔細了,其中 double 類型的 b 需要兩個連續(xù)的索引。取值的時候,取出的是2這個索引中的值。如果是靜態(tài)方法,則數(shù)組第0個不存放 this 引用,而是直接存儲傳遞的參數(shù)。 》操作數(shù)棧: 操作數(shù)棧中存放方法執(zhí)行時的一些中間變量,JVM 在執(zhí)行方法時壓入或者彈出這些變量。其實,操作數(shù)棧是方法真正工作的地方,執(zhí)行方法時,局部變量數(shù)組與操作數(shù)棧根據(jù)方法定義進行數(shù)據(jù)交換。例如,執(zhí)行以下代碼時,操作數(shù)棧的情況如下:

int a = 90;
int b = 10;
int c = a + b;

http://wiki.jikexueyuan.com/project/java-special-topic/images/31.jpg" alt="" />

注意在這個圖中,操作數(shù)棧的地步是在上邊,所以先壓入的100位于上方??梢钥闯觯僮鲾?shù)棧其實是一個數(shù)據(jù)臨時存儲區(qū),存放一些中間變量,方法結(jié)束了,操作數(shù)棧也就沒有啦。 》棧幀中數(shù)據(jù)引用: 除了局部變量數(shù)組和操作數(shù)棧之外,棧幀還需要一個常量池的引用。當 JVM 執(zhí)行到需要常量池的數(shù)據(jù)時,就是通過這個引用來訪問常量池的。棧幀中的數(shù)據(jù)還要負責處理方法的返回和異常。如果通過 return 返回,則將該方法的棧幀從 Java 棧中彈出。如果方法有返回值,則將返回值壓入到調(diào)用該方法的方法的操作數(shù)棧中。另外,數(shù)據(jù)區(qū)中還保存中該方法可能的異常表的引用。下面的例子用來說明:

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;
    }

}

執(zhí)行上述代碼時,Java 棧如下圖所示:

http://wiki.jikexueyuan.com/project/java-special-topic/images/32.jpg" alt="" />

花些時間好好研究上圖。一樣需要注意的是,棧的底部在上方,先押人員 addAndPrint 方法的棧幀,再壓入 addTwoTypes 方法的棧幀。上圖最右邊的文字說明有錯誤,應(yīng)該是 addTwoTypes 的執(zhí)行結(jié)果存放在 addAndPrint 的操作數(shù)棧中。 》》本地方法棧 當程序通過 JNI(Java Native Interface)調(diào)用本地方法(如 C 或者 C++ 代碼)時,就根據(jù)本地方法的語言類型建立相應(yīng)的棧。 》》方法區(qū)域 方法區(qū)域是一個 JVM 實例中的所有線程共享的,當啟動一個 JVM 實例時,方法區(qū)域被創(chuàng)建。它用于存運行放常量池、有關(guān)域和方法的信息、靜態(tài)變量、類和方法的字節(jié)碼。不同的 JVM 實現(xiàn)方式在實現(xiàn)方法區(qū)域的時候會有所區(qū)別。Oracle 的 HotSpot 稱之為永久區(qū)域(Permanent Area)或者永久代(Permanent Generation)。 》》運行常量池 這個區(qū)域存放類和接口的常量,除此之外,它還存放方法和域的所有引用。當一個方法或者域被引用的時候,JVM 就通過運行常量池中的這些引用來查找方法和域在內(nèi)存中的的實際地址。 》》堆(Heap) 堆中存放的是程序創(chuàng)建的對象或者實例。這個區(qū)域?qū)?JVM 的性能影響很大。垃圾回收機制處理的正是這一塊內(nèi)存區(qū)域。 所以,類加載器加載其實就是根據(jù)編譯后的 Class 文件,將 java 字節(jié)碼載入 JVM 內(nèi)存,并完成對運行數(shù)據(jù)處于的初始化工作,供執(zhí)行引擎執(zhí)行。

三、 執(zhí)行引擎(Execution Engine) 類加載器將字節(jié)碼載入內(nèi)存之后,執(zhí)行引擎以 Java 字節(jié)碼指令為但愿,讀取 Java 字節(jié)碼。問題是,現(xiàn)在的 java 字節(jié)碼機器是讀不懂的,因此還必須想辦法將字節(jié)碼轉(zhuǎn)化成平臺相關(guān)的機器碼。這個過程可以由解釋器來執(zhí)行,也可以有即時編譯器(JIT Compiler)來完成。

http://wiki.jikexueyuan.com/project/java-special-topic/images/33.jpg" alt="" />

上一篇:Socket下一篇:面向?qū)ο缶幊?/a>