1. 程式人生 > >Java虛擬機器的簡單理解

Java虛擬機器的簡單理解

內容比較枯燥,但是要用心去看,畢竟面試要問

java大環境

Java不單單是一種語言具體來說它是一種技術,由四方面組成:Java程式語言、Java類檔案格式、Java虛擬機器和Java應用程式介面(Java API)。它們的關係如下圖所示:
在這裡插入圖片描述
執行期環境代表著Java平臺,開發人員編寫Java程式碼(.java檔案),然後將之編譯成位元組碼(.class檔案),再然後位元組碼被裝入記憶體,一旦位元組碼進入虛擬機器,它就會被直譯器解釋執行,或者是被即時程式碼發生器有選擇的轉換成機器碼執行。

認識JVM

Java平臺由Java虛擬機器和Java應用程式介面搭建,Java語言則是進入這個平臺的通道,用Java語言編寫並編譯的程式可以執行在這個平臺上。
java平臺結構
在這裡插入圖片描述


Java虛擬機器(JVM) 處在核心的位置,是程式與底層作業系統和硬體無關的關鍵。
1.它的下方是移植介面,移植介面由兩部分組成:介面卡和Java作業系統, 其中依賴於平臺的部分稱為介面卡;
JVM 通過移植介面在具體的平臺和作業系統上實現;

2.在JVM 的上方是Java的基本類庫和擴充套件類庫以及它們的API, 利用Java API編寫的應用程式(application) 和小程式(Java applet) 可以在任何Java平臺上執行而無需考慮底層平臺。

就是因為有Java虛擬機器(JVM)實現了程式與作業系統的分離,從而實現了Java 的平臺無關性。

JVM在它的生存週期中有一個明確的任務,那就是執行Java程式,因此當Java程式啟動的時候,就產生JVM的一個例項;當程式執行結束的時候,該例項也跟著消失了。

下面我們從JVM的體系結構和它的執行過程這兩個方面來對它進行比較深入的研究。

深入瞭解JVM

Java虛擬機器的體系結構

每個JVM都有兩種機制:
①類裝載子系統:裝載具有適合名稱的類或介面
②執行引擎:負責執行包含在已裝載的類或介面中的指令

每個JVM都包含:
方法區、Java堆、Java棧、本地方法棧、指令計數器及其他隱含暫存器
在這裡插入圖片描述

對於JVM的學習,在我看來這麼幾個部分最重要:

Java程式碼編譯和執行的整個過程

JVM記憶體管理及垃圾回收機制

Java程式碼編譯和執行的整個過程

也正如前面所說,Java程式碼的編譯和執行的整個過程大概是:開發人員編寫Java程式碼(.java檔案),然後將之編譯成位元組碼(.class檔案),再然後位元組碼被裝入記憶體,一旦位元組碼進入虛擬機器,它就會被直譯器解釋執行,或者是被即時程式碼發生器有選擇的轉換成機器碼執行。

(1)Java程式碼編譯是由Java原始碼編譯器來完成,也就是Java程式碼到JVM位元組碼(.class檔案)的過程。 流程圖如下所示:
在這裡插入圖片描述

(2)Java位元組碼的執行是由JVM執行引擎來完成,流程圖如下所示:
在這裡插入圖片描述

Java程式碼編譯和執行的整個過程包含了以下三個重要的機制:

·Java原始碼編譯機制
·類載入機制
·類執行機制

(1)Java原始碼編譯機制

Java 原始碼編譯由以下三個過程組成:
①分析和輸入到符號表
②註解處理
③語義分析和生成class檔案

流程圖如下所示:

在這裡插入圖片描述

最後生成的class檔案由以下部分組成:

①結構資訊:包括class檔案格式版本號及各部分的數量與大小的資訊

②元資料:對應於Java原始碼中宣告與常量的資訊。包含類/繼承的超類/實現的介面的宣告資訊、域與方法宣告資訊和常量池

③方法資訊:對應Java原始碼中語句和表示式對應的資訊。包含位元組碼、異常處理器表、求值棧與區域性變數區大小、求值棧的型別記錄、除錯符號資訊

(2)類載入機制

JVM的類載入是通過ClassLoader及其子類來完成的,類的層次關係和載入順序可以由下圖來描述:

在這裡插入圖片描述

①Bootstrap ClassLoader

負責載入$JAVA_HOME中jre/lib/rt.jar裡所有的class,由C++實現,不是ClassLoader子類

②Extension ClassLoader

負責載入java平臺中擴充套件功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包

③App ClassLoader

負責記載classpath中指定的jar包及目錄中class

④Custom ClassLoader

屬於應用程式根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader

載入過程中會先檢查類是否被已載入,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已載入就視為已載入此類,保證此類只所有ClassLoader載入一次。而載入的順序是自頂向下,也就是由上層來逐層嘗試載入此類。

(3)類執行機制

JVM是基於堆疊的虛擬機器。JVM為每個新建立的執行緒都分配一個堆疊.也就是說,對於一個Java程式來說,它的執行就是通過對堆疊的操作來完成的。堆疊以幀為單位儲存執行緒的狀態。JVM對堆疊只進行兩種操作:以幀為單位的壓棧和出棧操作。

JVM執行class位元組碼,執行緒建立後,都會產生程式計數器(PC)和棧(Stack),程式計數器存放下一條要執行的指令在方法內的偏移量,棧中存放一個個棧幀,每個棧幀對應著每個方法的每次呼叫,而棧幀又是有區域性變數區和運算元棧兩部分組成,區域性變數區用於存放方法中的區域性變數和引數,運算元棧中用於存放方法執行過程中產生的中間結果。棧的結構如下圖所示:

在這裡插入圖片描述

JVM記憶體管理及垃圾回收機制

記憶體管理

JVM記憶體結構分為:方法區(method),棧記憶體(stack),堆記憶體(heap),本地方法棧(java中的jni呼叫),結構圖如下所示:

在這裡插入圖片描述

(1)堆記憶體(heap)

所有通過new建立的物件的記憶體都在堆中分配,其大小可以通過-Xmx和-Xms來控制。
作業系統有一個記錄空閒記憶體地址的連結串列,當系統收到程式的申請時,會遍歷該連結串列,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點連結串列中刪除,並將該結點的空間分配給程式,另外,對於大多數系統,會在這塊記憶體空間中的首地址處記錄本次分配的大小,這樣程式碼中的delete語句才能正確的釋放本記憶體空間。但由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒連結串列中。這時由new分配的記憶體,一般速度比較慢,而且容易產生記憶體碎片,不過用起來最方便。另外,在WINDOWS下,最好的方式是用VirtualAlloc分配記憶體,它不是在堆,也不是在棧,而是直接在程序的地址空間中保留一塊記憶體,雖然這種方法用起來最不方便,但是速度快,也是最靈活的。堆記憶體是向高地址擴充套件的資料結構,是不連續的記憶體區域。由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。

(2)棧記憶體(stack)

在Windows下, 棧是向低地址擴充套件的資料結構,是一塊連續的記憶體區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是固定的(是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。只要棧的剩餘空間大於所申請空間,系統將為程式提供記憶體,否則將報異常提示棧溢位。 由系統自動分配,速度較快。但程式設計師是無法控制的。

堆記憶體與棧記憶體需要說明:

基礎資料型別直接在棧空間分配,方法的形式引數,直接在棧空間分配,當方法呼叫完成後從棧空間回收。引用資料型別,需要用new來建立,既在棧空間分配一個地址空間,又在堆空間分配物件的類變數 。方法的引用引數,在棧空間分配一個地址空間,並指向堆空間的物件區,當方法呼叫完成後從棧空間回收。區域性變數new出來時,在棧空間和堆空間中分配空間,當局部變數生命週期結束後,棧空間立刻被回收,堆空間區域等待GC回收。方法呼叫時傳入的literal引數,先在棧空間分配,在方法呼叫完成後從棧空間收回。字串常量、static在DATA區域分配,this在堆空間分配。陣列既在棧空間分配陣列名稱,又在堆空間分配陣列實際的大小。

如:
在這裡插入圖片描述

(3)本地方法棧(java中的jni呼叫)

用於支援native方法的執行,儲存了每個native方法呼叫的狀態。對於本地方法介面,實現JVM並不要求一定要有它的支援,甚至可以完全沒有。Sun公司實現Java本地介面(JNI)是出於可移植性的考慮,當然我們也可以設計出其它的本地介面來代替Sun公司的JNI。但是這些設計與實現是比較複雜的事情,需要確保垃圾回收器不會將那些正在被本地方法呼叫的物件釋放掉。

(4)方法區(method)

它儲存方法程式碼(編譯後的java程式碼)和符號表。存放了要載入的類資訊、靜態變數、final型別的常量、屬性和方法資訊。JVM用持久代(Permanet Generation)來存放方法區,可通過-XX:PermSize和-XX:MaxPermSize來指定最小值和最大值。

垃圾回收機制

堆裡聚集了所有由應用程式建立的物件,JVM也有對應的指令比如 new, newarray, anewarray和multianewarray,然並沒有向 C++ 的 delete,free 等釋放空間的指令,Java的所有釋放都由 GC 來做,GC除了做回收記憶體之外,另外一個重要的工作就是記憶體的壓縮,這個在其他的語言中也有類似的實現,相比 C++ 不僅好用,而且增加了安全性,當然她也有弊端,比如效能這個大問題。

Java虛擬機器的執行過程示例

上面對虛擬機器的各個部分進行了比較詳細的說明,下面通過一個具體的例子來分析它的執行過程。

虛擬機器通過呼叫某個指定類的方法main啟動,傳遞給main一個字串陣列引數,使指定的類被裝載,同時連結該類所使用的其它的型別,並且初始化它們。例如對於程式:

class HelloApp {

    public static void main(String[] args) {

        System.out.println("text");
        for (int i=0;i<args.length;i++){
            System.out.println(args[i]);
        }
        
    }
}

編譯後在命令列模式下鍵入: java HelloApp run virtual machine

將通過呼叫Test的方法main來啟動java虛擬機器,傳遞給main一個包含三個字串"run"、“virtual”、"machine"的陣列。現在我們略述虛擬機器在執行HelloApp時可能採取的步驟。

開始試圖執行類HelloApp的main方法,發現該類並沒有被裝載,也就是說虛擬機器當前不包含該類的二進位制代表,於是虛擬機器使用ClassLoader試圖尋找這樣的二進位制代表。如果這個程序失敗,則丟擲一個異常。類被裝載後同時在main方法被呼叫之前,必須對類HelloApp與其它型別進行連結然後初始化。連結包含三個階段:檢驗,準備和解析。檢驗檢查被裝載的主類的符號和語義,準備則建立類或介面的靜態域以及把這些域初始化為標準的預設值,解析負責檢查主類對其它類或介面的符號引用,在這一步它是可選的。類的初始化是對類中宣告的靜態初始化函式和靜態域的初始化構造方法的執行。一個類在初始化之前它的父類必須被初始化。整個過程如下:
在這裡插入圖片描述