初步瞭解JVM第一篇

你們都知道,Java中JVM的重要性,學習了JVM你對Java的運行機制、編譯過程和如何對Java程序進行調優相信都會有一個很好的認知。java

廢話很少說,直接帶你們來初步認識一下JVM。編程

  • 什麼是JVM?

JVM(Java Virtual Machine)是一個抽象的計算機,和實際的計算機同樣,它具備指令集並使用不一樣的存儲區域,它負責執行指令,還要管理數據、內存和寄存器。數據結構

看到這裏,可能不懂JVM的人,已經蒙圈了。不要緊,下面讓我詳細爲你們介紹JVM的體系架構圖,或許你會明白些。架構

簡單來講,JVM就是一個虛擬計算機。咱們都知道Java語言其中的一個特性就是跨平臺的,而JVM就是Java程序實現跨平臺的關鍵部分。Java編譯器編譯Java程序時,生成的是與平臺無關的字節碼(也就是*.class文件),所謂的平臺無關是指編譯生成的字節碼不管是在Window、Linux、Mac系統都是可執行。也就是說Java編譯生成的*.class文件不是面向平臺的,而是面向JVM的。不一樣平臺上的JVM都是不一樣的,可是他們都是提供了相同的接口。圖一爲Java的大體運行步驟:編程語言

 

 圖一學習

引用一個《瘋狂Java講義》中提到例子來幫助你們理解JVM的做用:spa

JVM的做用就像有兩隻不一樣的鉛筆,但須要把同一個筆帽套在兩支不一樣的筆上,只有爲這兩支筆分別提供一個轉換器,這個轉換器向上的接口相同,用於適應同一個筆帽;向下的接口不一樣,用於適應兩支不一樣的筆。在這個類比中,能夠近似地理解兩支不一樣的筆就是不一樣的操做系統,而同一個筆帽就是Java字節碼程序,轉換器角色則對應JVM。相似地,也能夠認爲JVM分爲向上和向下兩個部分,全部平臺的JVM向上提供給Java字節碼程序的接口徹底相同,但向下適應的不一樣平臺的接口則互不相同。操作系統

  • JVM體系結構概覽

上面咱們是初步介紹了JVM的做用,那麼要深刻去了解JVM咱們就須要瞭解JVM的體系結構,請看圖二:線程

圖二指針

圖二是JVM的體系架構圖,接下讓咱們一塊兒來聊一聊每個部分都是什麼意思。

1.類裝載器子系統(ClassLoader)

負責加載class文件,class文件在文件開頭有特定的文件標示,將class文件字節碼內容加載到內存中,並將這些內容轉換成方法區中的運行時數據結構而且ClassLoader只負責class文件的加載,至於它是否能夠運行,則由Execution Engine決定。

Java編譯生成的*.class文件就是經過ClassLoader進行加載的,那麼這裏就會有幾個問題:

  • ClassLoader如何知道*.class文件就是須要加載的文件?
  • 若是我手動將一個普通文件的擴展名稱改成class後綴,ClassLoader會加載這個文件嗎?

實際上,class文件在文件的開頭是有特定的文件標識的,隨便編寫一個Java程序,編譯生成一個class文件,打開後你都能看到以下內容:

cafe babe就是class文件的一個標識,ClassLoader負責加載有cafe babe的class文件,它將class文件字節碼內容加載到內存中,並將這些內容轉換成方法區中的運行時的數據結構而且ClassLoader只負責class文件的加載,至於它是否能夠運行,則由Execution Engine決定,請看圖三:

 

 

圖三

Car.class文件經過ClassLoader進行加載到內存中,Car Class在內存中就至關一個模板,咱們能夠經過這個模板能夠實例化成不一樣的實例car一、car二、car3。

不知你們會不會有一個疑問,ClassLoader加載Car.class在Java中是用什麼類型的加載器加載的呢?在解答這個問題前咱們先寫個簡單的代碼看看:

        //new一個Car對象
        Car car = new Car();

        //獲得ClassLoader
        ClassLoader classLoader = car.getClass().getClassLoader();

        //打印結果
        System.out.println(classLoader);

結果爲:

 咱們再來看看另一組代碼:

        //new兩個不一樣的對象
        Car car = new Car();
        String string = new String();

        //獲得ClassLoader
        ClassLoader classLoader1 = car.getClass().getClassLoader();
        ClassLoader classLoader2 = string.getClass().getClassLoader();

        //打印結果
        System.out.println(classLoader1);
        System.out.println(classLoader2);

結果爲:

 從上面咱們能夠知道,ClassLoader的打印結果一個是「sun.misc.Launcher$AppClassLoader@18b4aac2」,一個則是「null」,這是怎麼回事呢,細心的朋友就能夠發現這兩個不一樣的對象中,其中car對象是咱們本身寫的一個類,string對象是系統自帶的一個類。簡單來講就是ClassLoader會根據不一樣的類選擇不一樣的類加載器去進行加載。這裏就牽扯到了ClassLoader的分類

ClassLoader的類別:

  • 啓動類加載器(BootStrap)
  • 擴展類加載器(Extension)
  • 應用程序類加載器(AppClassLoader)
  • 用戶自定義加載器

通常咱們本身所寫的類用的類加載器都是AppClassLoader,就是上圖所示的「sun.misc.Launcher$AppClassLoader@18b4aac2」,而爲何string這個對象是」null「呢?實際上,這個「null」指的就是使用BootStrap這個加載器。

那可能有人有疑問,本身定義的類用AppClassLoader,能理解,由於car這個對象輸出的類加載器名字中有AppClassLoader這個字樣,可是爲何string這個對象是」null「,從哪裏可用體現是用BootStrap這個加載器呢?是這樣的,BootStrap累加載器至關於擴展類加載器、應用程序類加載器的祖宗,如果用了BootStrap,因爲BootStrap上一級已經沒有了,因此就用「null」來表示

其實咱們能夠找一下String這個類在JDK的位置:

$JAVA_HOME/jre/lib/rt.jar/java/lang

全部在這個路徑$JAVA_HOME/jre/lib/rt.jar這個jar包下的類都是用BootStrap來加載的。

下面請看圖4:

 

 

圖四 

這張圖就能夠很清晰得看到:

1.全部在$Java_Home/jre/lib/rt.jar是經過BootStrap加載的

2.全部在$Java_Home/jre/lib/ext/*.jar是經過Extension加載的

3.全部在$CLASSPATH是經過SYSTEM加載的(應用程序類加載器也叫系統類加載器,加載當前應用的classpath的全部類)

接下來咱們再來看一個例子:

若是建立一個java.lang包,而後建立String類,打印一句話執行會怎麼樣呢?

package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

效果以下:

 能夠看到程序報錯了,說是找不到main方法,但是明明就有main方法爲何沒有執行呢?這裏就涉及了雙親委派機制

雙親委派機制:

當一個類收到了類加載請求,他首先不會嘗試本身去加載這個類,而是把這個請求委派給父類去完成,每個層次類加載器都是如此,所以全部的加載請求都應該傳送到啓動類加載器中,只有當父類加載器反饋本身沒法完成這個請求的時候(在它的加載路徑下沒有找到所需加載的Class),子類加載器纔會嘗試本身去加載。

因此它實際的運行過程是這樣的:

  • ClassLoader收到String類的加載請求。
  • 先去Bootstrap查找是否有這個類,沒有則反饋沒法完成這個請求,可是剛好,在rt.jar中找到了java.lang.Stirng這個類
  • 執行這個類,這個類是沒有定義main方法的
  • 報錯,類中沒有定義main方法

因此上面的例子,他會找到jdk中java.lang.String這個類,這個類確實是沒有定義main方法,簡單來講它執行的類是JDK中java.lang.String這個類,而不是咱們本身定義的類。

那用雙親委派機制有什麼好處呢:

採用雙親委派的一個好處是好比加載位於 rt.jar 包中的類 java.lang.Object,不論是哪一個加載器加載這個類,最終都是委託給頂層的啓動類加載器進行加載,這樣就保證了使用不一樣的類加載器最終獲得的都是一樣一個 Object對象。

2.執行引擎(Execution Engine)

執行引擎負責解釋命令,提交給操做系統執行,這裏對執行引擎就不作過多的解釋了,只要知道他是負責解釋命令的便可。

3.本地方法接口(Native Interface)和本地方法棧(Native Method Stack)

  • 本地接口:本地接口的做用是融合不一樣的編程語言爲 Java 所用,它的初衷是融合 C/C++程序,Java 誕生的時候是 C/C++橫行的時候,要想立足,必須有調用 C/C++程序,因而就在內存中專門開闢了一塊區域處理標記爲native的代碼,它的具體作法是 Native Method Stack中登記 native方法,在Execution Engine 執行時加載native libraies。

   目前該方法使用的愈來愈少了,除非是與硬件有關的應用,好比經過Java程序驅動打印機或者Java系統管理生產設備,在企業級應用中已經比較少見。由於如今的異構領域間的通訊很發達,好比可使用    Socket通訊,也可使用Web Service等等,很少作介紹。

          若是在程序中有見到native關鍵字,就表明不是Java能完成的事情了,須要加載本地方法庫才能完成

  • 本地方法棧:它的具體作法是Native Method Stack中登記native方法,在Execution Engine 執行時加載本地方法庫。說白了就是本地方法由本地方法棧來登記,Java中的方法由Java棧來登記。

4.PC寄存器(Program Counter Register)

每一個線程都有一個程序計數器,是線程私有的,就是一個指針,指向方法區中的方法字節碼(用來存儲指向下一條指令的地址,也即將要執行的指令代碼),由執行引擎讀取下一條指令,是一個很是小的內存空間,幾乎能夠忽略不記。
這塊內存區域很小,它是當前線程所執行的字節碼的行號指示器,字節碼解釋器經過改變這個計數器的值來選取下一條須要執行的字節碼指令。
若是執行的是一個Native方法,那這個計數器是空的。
PC寄存器用來完成分支、循環、跳轉、異常處理、線程恢復等基礎功能。因爲使用的內存較小,因此不會發生內存溢出(OutOfMemory)錯誤。

那麼這篇文章先講到這裏,下篇文章中咱們再繼續來聊一聊方法區、棧和堆..........

相關文章
相關標籤/搜索