JVM之初體驗

     目前Java是一門主流的編程語言,其熱門程度可想而知。然而,Java這門語言的易上手也導致了很多程序員對其基礎知其然而不知其所以然。今天我們就來詳細介紹下JVM。本文參考:Unserstading JVM InternalsJVM內存管理【譯】深入理解JVM

目錄

一 Java虛擬機規範

二 JVM定義

三 JVM原理

四 JRE/JDK/JVM三者間的關係

五 JVM的基本特性

六 JVM的體系結構

6.1類加載

6.2運行時數據區

6.3執行引擎

 


一 Java虛擬機規範

     Java虛擬機規範是一種對Java虛擬機實現的規範要求,是有oracle制定的。而我們平時常說的Java虛擬機一般指的是一種具體的實現。業界目前有多種不同的JVM實現,包括Oracle Hostpot和BIM JVM。

二 JVM定義

     JVM是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM包括一套字節碼指令集、一組寄存器、一個棧、一個垃圾回收堆和一個存儲方法域。

三 JVM原理

    JVM是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器。它是一種利用軟件方法實現的抽象的計算機基於下層的操作系統和硬件平臺,JVM使用JAVA字節碼語言,一種運行於JAVA(用戶語言)和機器語言的中間語言。Java字節碼是部署Java程序的最小單元。

Java編譯器只要面向JVM,生成JVM能理解的代碼或字節碼文件。Java源文件經編譯成字節碼程序,通過JVM將每一條指令翻譯成不同平臺機器碼,通過特定平臺運行。

 

四 JRE/JDK/JVM三者間的關係

JRE(JavaRuntimeEnvironment,Java運行環境),也就是Java平臺。所有的Java 程序都要在JRE下才能運行。普通用戶只需要運行已開發好的java程序,安裝JRE即可。

JDK(Java Development Kit)是程序開發者用來來編譯、調試java程序用的開發工具包。JDK的工具也是Java程序,也需要JRE才能運行。爲了保持JDK的獨立性和完整性,在JDK的安裝過程中,JRE也是 安裝的一部分。所以,在JDK的安裝目錄下有一個名爲jre的目錄,用於存放JRE文件。

JVM(JavaVirtualMachine,Java虛擬機)是JRE的一部分。它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。Java語言最重要的特點就是跨平臺運行。使用JVM就是爲了支持與操作系統無關,實現跨平臺。

ps:JRE由Java API和JVM(核心)組成,JVM通過類加載器(Class Loader)加類Java應用,並通過Java API進行執行。

五 JVM的基本特性

  1. 基於棧(Stack-based)的虛擬機: 不同於Intel x86和ARM等比較流行的計算機處理器都是基於寄存器(register)架構,JVM是基於棧執行的
  2. 符號引用(Symbolic reference): 除基本類型外的所有Java類型(類和接口)都是通過符號引用取得關聯的,而非顯式的基於內存地址的引用。
  3. 垃圾回收機制: 類的實例通過用戶代碼進行顯式創建,但卻通過垃圾回收機制自動銷燬。
  4. 通過明確清晰基本類型確保平臺無關性: 像C/C++等傳統編程語言對於int類型數據在同平臺上會有不同的字節長度。JVM卻通過明確的定義基本類型的字節長度來維持代碼的平臺兼容性,從而做到平臺無關。
  5. 網絡字節序(Network byte order): Java class文件的二進制表示使用的是基於網絡的字節序(network byte order)。爲了在使用小端(little endian)的Intel x86平臺和在使用了大端(big endian)的RISC系列平臺之間保持平臺無關,必須要定義一個固定的字節序。JVM選擇了網絡傳輸協議中使用的網絡字節序,即基於大端(big endian)的字節序。

六 JVM的體系結構

  • 類裝載器(ClassLoader)(用來裝載.class文件)
  • 執行引擎(執行字節碼,或者執行本地方法)
  • 運行時數據區(方法區、堆、java棧、PC寄存器、本地方法棧)

接下來讓我們深入解析JVM結構,首先Java程序的執行過程如下圖:

類加載器把Java字節碼載入到運行時數據區,執行引擎負責Java字節碼的執行。

6.1類加載

Java提供了動態加載的特性,只有在運行時第一次遇到類時纔會去加載和鏈接,而非在編譯時加載它。JVM的類加載器負責類的動態加載過程。Java類加載器的特點如下:

  • 層次結構:Java的類加載器按是父子關係的層次結構組織的。Boostrap類加載器處於層次結構的頂層,是所有類加載器的父類。
  • 代理模型:基於類加載器的層次組織結構,類加載器之間是可以進行代理的。當一個類需要被加載,會先去請求父加載器判斷該類是否已經被加載。如果父類加器已加載了該類,那它就可以直接使用而無需再次加載。如果尚未加載,才需要當前類加載器來加載此類。
  • 可見性限制:子類加載器可以從父類加載器中獲取類,反之則不行。
  • 不能卸載: 類加載器可以載入類卻不能卸載它。但是可以通過刪除類加載器的方式卸載類。

每個類加載器都有自己的空間,用於存儲其加載的類信息。當類加載器需要加載一個類時,它通過FQCN)(Fully Quanlified Class Name: 全限定類名)的方式先在自己的存儲空間中檢測此類是否已存在。在JVM中,即便具有相同FQCN的類,如果出現在了兩個不同的類加載器空間中,它們也會被認爲是不同的。存在於不同的空間意味着類是由不同的加載器加載的。

下圖解釋了類加載器的代理模型:

當JVM請示類加載器加載一個類時,加載器總是按照從類加載器緩存、父類加載器以及自己加載器的順序查找和加載類。也就是說加載器會先從緩存中判斷此類是否已存在,如果不存在就請示父類加載器判斷是否存在,如果直到Bootstrap類加載器都不存在該類,那麼當前類加載器就會從文件系統中找到類文件進行加載。

  • Bootstrap加載器:Bootstrap加載器在運行JVM時創建,用於加載Java APIs,包括Object類。不像其他的類加載器由Java代碼實現,Bootstrap加載器是由native代碼實現的。
  • 擴展加載器(Extension class loader):擴展加載器用於加載除基本Java APIs以外擴展類。也用於加載各種安全擴展功能。
  • 系統加載器(System class loader):如果說Bootstrap和Extension加載器用於加載JVM運行時組件,那麼系統加載器加載的則是應用程序相關的類。它會加載用戶指定的CLASSPATH裏的類。
  • 用戶自定義加載器:這個是由用戶的程序代碼創建的類加載器。

像Web應用服務器(WAS: Web Application Server)等框架通過使用用戶自定義加載器使Web應用和企業級應用可以隔離開在各自的類加載空間獨自運行。也就是說可以通過類加載器的代理模型來保證應用的獨立性。不同的WAS在自定義類加載器時會有略微不同,但都不外乎使用加載器的層次結構原理。

如果一個類加載器發現了一個未加載的類,則該類的加載和鏈接過程如下圖:

類加載步驟

每一步的具體描述如下:

  • 加載(Loading): 從文件中獲取類並載入到JVM內存空間。
  • 驗證(Verifying): 驗證載入的類是否符合Java語言規範和JVM規範。在類加載流程的測試過程中,這一步是最爲複雜且耗時最長的部分。大部分JVM TCK的測試用例都用於檢測對於給定的錯誤的類文件是否能得到相應的驗證錯誤信息。
  • 準備(Preparing): 根據內存需求準備相應的數據結構,並分別描述出類中定義的字段、方法以及實現的接口信息。
  • 解析(Resolving): 把類常量池中所有的符號引用轉爲直接引用。
  • 初始化(Initializing): 爲類的變量初始化合適的值。執行靜態初始化域,併爲靜態字段初始化相應的值。

JVM規範定義了規則,但也允許在運行時靈活處理。

6.2運行時數據區

運行時數據區結構

運行時數據區是JVM程序運行時在操作系統上分配的內存區域。運行時數據區又可細分爲6個部分,即:爲每個線程分別創建的PC寄存器JVM棧本地方法棧和被所有線程共用的數據堆方法區運行時常量池

  • PC 寄存器(線程獨有):全稱是程序計數寄存器,它記載着每一個線程當前運行的JAVA方法的地址,如果是當前執行的是本地方法,則程序計數器會是一個空地址。它的作用就是用來支持多線程,線程的阻塞、恢復、掛起等一系列操作,直觀的想象一下,要是沒有記住每個線程當前運行的位置,又如何恢復呢。依據這一點,每一個線程都有一個PC寄存器,也就是說PC寄存器是線程獨有的。
  • JVM 棧:每個線程都有一個JVM棧,並跟隨線程的啓動而創建。其中存儲的數據無素稱爲棧幀(Stack Frame)。JVM會每把棧楨壓入JVM棧或從中彈出一個棧幀。如果有任何異常拋出,像printStackTrace()方法輸出的棧跟蹤信息的每一行表示一個棧幀。 

    • 棧幀:棧幀是隨着方法的創建而創建,隨着方法的結束而銷燬,如果方法拋出異常,也算方法結束。棧幀中存放着對本地(native)變量數組、操作數棧以及屬於當前運行方法的運行時常量池的引用。本地變量數組和操作數棧的大小在編譯時就已確定,所以在運行時屬於方法的棧幀大小是固定的。
    • 本地變量數組:本地變量數組的索引從0開始計數,其位置存儲着對方法所屬類實例的引用。從索引位置1開始的保存的是傳遞給該方法的參數。其後存儲的就是真正的方法的本地變量了。
    • 操作數棧:是方法的實際運行空間。它是一個後進先出(LIFO)棧,而它的長度也是在編譯時期就寫入了class文件當中,是固定的。它的作用就是提供字節碼指令操作變量計算的空間,比如簡單的,對於int a=9這句話來說,就需要先將9壓入操作數棧,再將9賦給a這個變量。
  • 本地方法棧(線程獨有)本地方法棧是一個傳統的棧,它用來支持native方法的執行。如果JAVA虛擬機是使用的其它語言實現指令集解釋器的時候,也會用到本地方法棧。如果前面這兩種都未發生,也就是說如果JAVA虛擬機不依賴於本地方法棧,而且JAVA虛擬機也不支持native方法,則不需要本地方法棧。而如果需要的話,則本地方法棧也是隨每一個線程的啓動而創建的。

  • 方法區(全局共享):方法區是被所有線程共用的內存空間,在JVM啓動時創建。它存儲了運行時常量池、字段和方法信息、靜態變量以及被JVM載入的所有類和接口的方法的字節碼。不同的JVM提供者在實現方法區時會通常有不同的形式。在Oracle的Hotspot JVM裏方法區被稱爲Permanent Area(永久區)或Permanent Generation(PermGen, 永久代)。這一部分JAVA虛擬機規範不強制要求實現自動內存管理系統(GC)。

  • 運行時常量池:一個存儲了類文件格式中的常量池表的內存空間。這部分空間雖然存在於方法區內,但卻在JVM操作中扮演着舉足輕重的角色,因此JVM規範單獨把這一部分拿出來描述。除了每個類或接口中定義的常量,它還包含了所有對方法和字段的引用因此當需要一個方法或字段時,JVM通過運行時常量池中的信息從內存空間中來查找其相應的實際地址。

  • 數據堆(全局共享):堆中存儲着所有的類實例或對象,並且也是垃圾回收的目標場所。當涉及到JVM性能優化時,通常也會提及到數據堆空間的大小設置。JVM提供者可以決定劃分堆空間或者不執行垃圾回收。

在JVM運行時,每個類的實例被分配到數據堆上,類信息(包括User, UserAdmin, UserService, String)等被存儲在方法區。

6.3執行引擎

JVM通過類加載器把字節碼載入運行時數據區是由執行引擎執行的。執行引擎以指令爲單位讀入Java字節碼,就像CPU一個接一個的執行機器命令一樣。每個字節碼命令包含一字節的操作碼和可選的操作數。執行引擎讀取一個指令並執行相應的操作數,然後去讀取並執行下一條指令。

儘管如此,Java字節碼還是以一種可以理解的語言編寫的,而不像那些機器直接執行的無法讀懂的語言。所以JVM的執行引擎必須要把字節碼轉換爲能被機器執行的語言指令。執行引擎有兩種常用的方法來完成這一工作:

  • 解釋器(Interpreter):讀取、解釋並逐一執行每一條字節碼指令。因爲解釋器逐一解釋和執行指令,因此它能夠快速的解釋每一個字節碼,但對解釋結果的執行速度較慢。所有的解釋性語言都有類似的缺點。叫做字節碼的語言人本質上就像一個解釋器一樣運行。
  • 即時編譯器(JIT: Just-In-Time):即時編譯器的引入用來彌補解釋器的不足。執行引擎先以解釋器的方式運行,然後在合適的時機,即時編譯器把整修字節碼編譯成本地代碼。然後執行引擎就不再解釋方法的執行而是通過使用本地代碼直接執行。執行本地代碼較逐一解釋執行每條指令在速度上有較大的提升,並且通過對本地代碼的緩存,編譯後的代碼能具有更快的執行速度。

然而,即時編譯器在編譯代碼時比逐一解釋和執行每條指令更耗時,所以如果代碼只會被執行一次,解釋執行可能會具有更好的性能。所以JVM通過檢查方法的執行頻率,然後只對達到一定頻率的方法纔會做即時編譯。

JVM規範中並未強行約束執行引擎如何運行。所以不同的JVM在實現各種的執行引擎時通過各種技術手段並引入多種即時編譯器來提升性能。

大部分的即時編譯器運行流程如下圖:

即時編譯器先把字節碼轉爲一種中間形式的表達式(IR: Itermediate Representation),並對之進行優化,然後再把這種表達式轉爲本地代碼。

Oracel Hotspot VM使用的即時編譯器稱爲Hotspot編譯器。之所以稱爲Hotspot是因爲Hotspot Compiler會根據分析找到具有更高編譯優先級的熱點代碼,然後所這些熱點代碼轉爲本地代碼。如果一個被編譯過的方法不再被頻繁調用,也即不再是熱點代碼,Hotspot VM會把這些本地代碼從緩存中刪除並對其再次使用解釋器模式執行。Hotspot VM有Server VM和Client VM之後,它們所使用的即時編譯器也有所不同。

Client VM和Server VM使用相同的運行時環境,如上圖所示,它們的區別在於使用了不同的即時編譯器。Server VM通過使用多種更爲複雜的性能優化技術從而具有更好的表現。

IBM VM在他的IBM JDK6中引入了AOT(Ahead-Of-Time) 編譯器技術。通過此種技術使得多個JVM之間能通過共享緩存分享已編譯的本地代碼。也就是說通過AOT編譯器編譯的代碼能被其他JVM直接使用而無須再次編譯。另外IBM JVM通過使用AOT編譯器把代碼預編譯爲JXE(Java Executable)文件格式從而提供了一種快速執行代碼的方式。

大多數的Java性能提升都是通過優化執行引擎的性能實現的。像即時編譯等各種優化技術被不斷的引入,從而使得JVM性能得到了持續的優化和提升。老舊的JVM與最新的JVM之間最大的差異其實就來自於執行引擎的提升。

Hotspot編譯器從Java 1.3開始便引入到了Oracle Hotspot VM中,而即時編譯器從Android 2.2開始便被引入到了Android Dalvik VM中。

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章