理解Java虛擬機體系結構

概述

  衆所周知,Java支持平臺無關性、安全性和網絡移動性。而Java平臺由Java虛擬機和Java核心類所構成,它爲純Java程序提供了統一的編程接口,而無論下層操做系統是什麼。正是得益於Java虛擬機,它號稱的「一次編譯,處處運行」纔能有所保障。html

1.1 Java程序執行流程

  Java程序的執行依賴於編譯環境和運行環境。源碼代碼轉變成可執行的機器代碼,由下面的流程完成:java

  Java技術的核心就是Java虛擬機,由於全部的Java程序都在虛擬機上運行。Java程序的運行須要Java虛擬機、Java API和Java Class文件的配合。Java虛擬機實例負責運行一個Java程序。當啓動一個Java程序時,一個虛擬機實例就誕生了。當程序結束,這個虛擬機實例也就消亡。程序員

  Java的跨平臺特性,由於它有針對不一樣平臺的虛擬機。編程

1.2 Java虛擬機

  Java虛擬機的主要任務是裝載class文件而且執行其中的字節碼。由下圖能夠看出,Java虛擬機包含一個類裝載器(class loader),它能夠從程序和API中裝載class文件,Java API中只有程序執行時須要的類纔會被裝載,字節碼由執行引擎來執行。bootstrap

  當Java虛擬機由主機操做系統上的軟件實現時,Java程序經過調用本地方法和主機進行交互。Java方法由Java語言編寫,編譯成字節碼,存儲在class文件中。本地方法由C/C++/彙編語言編寫,編譯成和處理器相關的機器代碼,存儲在動態連接庫中,格式是各個平臺專有。因此本地方法是聯繫Java程序和底層主機操做系統的鏈接方式。數組

  因爲Java虛擬機並不知道某個class文件是如何被建立的,是否被篡改一無所知,因此它實現了一個class文件檢測器,確保class文件中定義的類型能夠安全地使用。class文件檢驗器經過四趟獨立的掃描來保證程序的健壯性:安全

  • class文件的結構檢查
  • 類型數據的語義檢查
  • 字節碼驗證
  • 符號引用驗證

  Java虛擬機在執行字節碼時還進行其它的一些內置的安全機制的操做,他們做爲Java編程語言保證Java程序健壯性的特性,同時也是Java虛擬機的特性:網絡

  • 類型安全的引用轉換
  • 結構化的內存訪問
  • 自動垃圾收集
  • 數組邊界檢查
  • 空引用檢查

1.3 Java虛擬機數據類型

  Java虛擬機經過某些數據類型來執行計算。數據類型能夠分爲兩種:基本類型和引用類型,以下圖:oracle

  但boolean有點特別,當編譯器把Java源碼編譯爲字節碼時,它會用int或byte表示boolean。在Java虛擬機中,false是由0表示,而true則由全部非零整數表示。和Java語言同樣,Java虛擬機的基本類型的值域在任何地方都是一致的,無論主機平臺是什麼,一個long在任何虛擬機中老是一個64位二進制補碼的有符號整數。app

  對於returnAddress,這個基本類型被用來實現Java程序中的finally子句,Java程序員不能使用這個類型,它的值指向一條虛擬機指令的操做碼。

2 體系結構

  在 Java虛擬機規範中,一個虛擬機實例的行爲是分別按照子系統、內存區、數據類型和指令來描述的,這些組成部分一塊兒展現了抽象的虛擬機的內部體系結構。

2.1 class文件

  Java class文件包含了關於類或接口的全部信息。class文件的「基本類型」以下:

u1 1個字節,無符號類型
u2 2個字節,無符號類型
u4 4個字節,無符號類型
u8 8個字節,無符號類型

  若是想了解更多,Oracle的JVM SE7給出了官方規範:The Java® Virtual Machine Specification

  class文件包含的內容:

複製代碼

ClassFile {

    u4 magic;                                     //魔數:0xCAFEBABE,用來判斷是不是Java class文件
    u2 minor_version;                             //次版本號
    u2 major_version;                             //主版本號
    u2 constant_pool_count;                       //常量池大小
    cp_info constant_pool[constant_pool_count-1]; //常量池
    u2 access_flags;                              //類和接口層次的訪問標誌(經過|運算獲得)
    u2 this_class;                                //類索引(指向常量池中的類常量)
    u2 super_class;                               //父類索引(指向常量池中的類常量)
    u2 interfaces_count;                          //接口索引計數器
    u2 interfaces[interfaces_count];              //接口索引集合
    u2 fields_count;                              //字段數量計數器
    field_info fields[fields_count];              //字段表集合
    u2 methods_count;                             //方法數量計數器
    method_info methods[methods_count];           //方法表集合
    u2 attributes_count;                          //屬性個數
    attribute_info attributes[attributes_count];  //屬性表

}

複製代碼

2.2 類裝載器子系統

  類裝載器子系統負責查找並裝載類型信息。其實Java虛擬機有兩種類裝載器:系統裝載器和用戶自定義裝載器。前者是Java虛擬機實現的一部分,後者則是Java程序的一部分。

  • 啓動類裝載器(bootstrap class loader):它用來加載 Java 的核心庫,是用原生代碼來實現的,並不繼承自java.lang.ClassLoader。
  • 擴展類裝載器(extensions class loader):它用來加載 Java 的擴展庫。Java 虛擬機的實現會提供一個擴展庫目錄。該類加載器在此目錄裏面查找並加載 Java 類。
  • 應用程序類裝載器(application class loader):它根據 Java 應用的類路徑(CLASSPATH)來加載 Java 類。通常來講,Java 應用的類都是由它來完成加載的。能夠經過 ClassLoader.getSystemClassLoader()來獲取它。

  除了系統提供的類裝載器之外,開發人員能夠經過繼承 java.lang.ClassLoader類的方式實現本身的類裝載器,以知足一些特殊的需求。

  類裝載器子系統涉及Java虛擬機的其它幾個組成部分以及來自java.lang庫的類。ClassLoader定義的方法爲程序提供了訪問類裝載器機制的接口。此外,對於每個被裝載的類型,Java虛擬機都會爲它建立一個java.lang.Class類的實例來表明該類型。和其它對象同樣,用戶自定義的類裝載器以及Class類的實例放在內存中的堆區,而裝載的類型信息則位於方法區。

  類裝載器子系統除了要定位和導入二進制class文件外,還必須負責驗證被導入類的正確性,爲類變量分配並初始化內存,以及解析符號引用。這些動做還須要按照如下順序進行:

  • 裝載(查找並裝載類型的二進制數據)
  • 鏈接(執行驗證:確保被導入類型的正確性;準備:爲類變量分配內存,並將其初始化爲默認值;解析:把類型中的符號引用轉換爲直接引用)
  • 初始化(類變量初始化爲正確初始值)

2.3 方法區

  在Java虛擬機中,關於被裝載的類型信息存儲在一個方法區的內存中。當虛擬機裝載某個類型時,它使用類裝載器定位相應的class文件,而後讀入這個class文件並將它傳輸到虛擬機中,接着虛擬機提取其中的類型信息,並將這些信息存儲到方法區。方法區也能夠被垃圾回收器收集,由於虛擬機容許經過用戶定義的類裝載器來動態擴展Java程序。

  方法區中存放了如下信息:

  • 這個類型的全限定名(如全限定名java.lang.Object)
  • 這個類型的直接超類的全限定名
  • 這個類型是類類型仍是接口類型
  • 這個類型的訪問修飾符(public, abstract, final的某個子集)
  • 任何直接超接口的全限定名的有序列表
  • 該類型的常量池(一個有序集合,包括直接常量[string, integer和floating point常量]和對其它類型、字段和方法的符號引用)
  • 字段信息(字段名、類型、修飾符)
  • 方法信息(方法名、返回類型、參數數量和類型、修飾符)
  • 除了常量之外的全部類(靜態)變量
  • 指向ClassLoader類的引用(每一個類型被裝載時,虛擬機必須跟蹤它是由啓動類裝載器仍是由用戶自定義類裝載器裝載的)
  • 指向Class類的引用(對於每個被裝載的類型,虛擬機相應地爲它建立一個java.lang.Class類的實例。好比你有一個到java.lang.Integer類的對象的引用,那麼只須要調用Integer對象引用的getClass()方法,就能夠獲得表示java.lang.Integer類的Class對象)

2.4 堆

  Java程序在運行時建立的全部類實例或數組(數組在Java虛擬機中是一個真正的對象)都放在同一個堆中。因爲Java虛擬機實例只有一個堆空間,因此全部線程都將共享這個堆。須要注意的是,Java虛擬機有一條在堆中分配對象的指令,卻沒有釋放內存的指令,由於虛擬機把這個任務交給垃圾收集器處理。Java虛擬機規範並無強制規定垃圾收集器,它只要求虛擬機實現必須「以某種方式」管理本身的堆空間。好比某個實現可能只有固定大小的堆空間,當空間填滿,它就簡單拋出OutOfMemory異常,根本不考慮回收垃圾對象的問題,但倒是符合規範的。

  Java虛擬機規範並無規定Java對象在堆中如何表示,這給虛擬機的實現者決定怎麼設計。一個可能的堆設計以下:

  一個句柄池,一個對象池。一個對象的引用就是一個指向句柄池的本地指針。這種設計的好處有利於堆碎片的整理,當移動對象池中的對象時,句柄部分只需更改一下指針指向對象的新地址便可。缺點是每次訪問對象的實例變量都要通過兩次指針傳遞。

2.5 Java棧

  每當啓動給一個線程時,Java虛擬機會爲它分配一個Java棧。Java棧由許多棧幀組成,一個棧幀包含一個Java方法調用的狀態。當線程調用一個Java方法時,虛擬機壓入一個新的棧幀到該線程的Java棧中,當該方法返回時,這個棧幀就從Java棧中彈出。Java棧存儲線程中Java方法調用的狀態--包括局部變量、參數、返回值以及運算的中間結果等。Java虛擬機沒有寄存器,其指令集使用Java棧來存儲中間數據。這樣設計的緣由是爲了保持Java虛擬機的指令集儘可能緊湊,同時也便於Java虛擬機在只有不多通用寄存器的平臺上實現。另外,基於棧的體系結構,也有助於運行時某些虛擬機實現的動態編譯器和即時編譯器的代碼優化。

2.5.1 棧幀

  棧幀由局部變量區、操做數棧和幀數據區組成。當虛擬機調用一個Java方法時,它從對應類的類型信息中獲得此方法的局部變量區和操做數棧的大小,並根據此分配棧幀內存,而後壓入Java棧中。

2.5.1.1 局部變量區

  局部變量區被組織爲以字長爲單位、從0開始計數的數組。字節碼指令經過從0開始的索引使用其中的數據。類型爲int, float, reference和returnAddress的值在數組中佔據一項,而類型爲byte, short和char的值在存入數組前都被轉換爲int值,也佔據一項。但類型爲long和double的值在數組中卻佔據連續的兩項。

2.5.1.2 操做數棧

  和局部變量區同樣,操做數棧也是被組織成一個以字長爲單位的數組。它經過標準的棧操做訪問--壓棧和出棧。因爲程序計數器沒法被程序指令直接訪問,Java虛擬機的指令是從操做數棧中取得操做數,因此它的運行方式是基於棧而不是基於寄存器。虛擬機把操做數棧做爲它的工做區,由於大多數指令都要從這裏彈出數據,執行運算,而後把結果壓回操做數棧。

2.5.1.3 幀數據區

  除了局部變量區和操做數棧,Java棧幀還須要幀數據區來支持常量池解析、正常方法返回以及異常派發機制。每當虛擬機要執行某個須要用到常量池數據的指令時,它會經過幀數據區中指向常量池的指針來訪問它。除了常量池的解析外,幀數據區還要幫助虛擬機處理Java方法的正常結束或異常停止。若是經過return正常結束,虛擬機必須恢復發起調用的方法的棧幀,包括設置程序計數器指向發起調用方法的下一個指令;若是方法有返回值,虛擬機須要將它壓入到發起調用的方法的操做數棧。爲了處理Java方法執行期間的異常退出狀況,幀數據區還保存一個對此方法異常表的引用。

2.6 程序計數器

  對於一個運行中的Java程序而言,每個線程都有它的程序計數器。程序計數器也叫PC寄存器。程序計數器既能持有一個本地指針,也能持有一個returnAddress。當線程執行某個Java方法時,程序計數器的值老是下一條被執行指令的地址。這裏的地址能夠是一個本地指針,也能夠是方法字節碼中相對該方法起始指令的偏移量。若是該線程正在執行一個本地方法,那麼此時程序計數器的值是「undefined」。

2.7 本地方法棧

  任何本地方法接口都會使用某種本地方法棧。當線程調用Java方法時,虛擬機會建立一個新的棧幀並壓入Java棧。當它調用的是本地方法時,虛擬機會保持Java棧不變,再也不在線程的Java棧中壓入新的棧,虛擬機只是簡單地動態鏈接並直接調用指定的本地方法。

其中方法區和堆由該虛擬機實例中全部線程共享。當虛擬機裝載一個class文件時,它會從這個class文件包含的二進制數據中解析類型信息,而後把這些類型信息放到方法區。當程序運行時,虛擬機會把全部該程序在運行時建立的對象放到堆中。

像其它運行時內存區同樣,本地方法棧佔用的內存區能夠根據須要動態擴展或收縮。

3 執行引擎

  在Java虛擬機規範中,執行引擎的行爲使用指令集定義。實現執行引擎的設計者將決定如何執行字節碼,實現能夠採起解釋、即時編譯或直接使用芯片上的指令執行,還能夠是它們的混合。

  執行引擎能夠理解成一個抽象的規範、一個具體的實現或一個正在運行的實例。抽象規範使用指令集規定了執行引擎的行爲。具體實現可能使用多種不一樣的技術--包括軟件方面、硬件方面或樹種技術的結合。做爲運行時實例的執行引擎就是一個線程。

  運行中Java程序的每個線程都是一個獨立的虛擬機執行引擎的實例。從線程生命週期的開始到結束,它要麼在執行字節碼,要麼執行本地方法。

3.1 指令集

  方法的字節碼流由Java虛擬機的指令序列構成。每一條指令包含一個單字節的操做碼,後面跟隨0個或多個操做數。操做碼錶示須要執行的操做;操做數向Java虛擬機提供執行操做碼須要的額外信息。當虛擬機執行一條指令時,可能使用當前常量池中的項、當前幀的局部變量中的值或者位於當前幀操做數棧頂端的值。

  抽象的執行引擎每次執行一條字節碼指令。Java虛擬機中運行的程序的每一個線程(執行引擎實例)都執行這個操做。執行引擎取得操做碼,若是操做碼有操做數,就取得它的操做數。它執行操做碼和跟隨的操做數規定的動做,而後再取得下一個操做碼。這個執行字節碼的過程在線程完成前將一直持續,經過從它的初始方法返回,或者沒有捕獲拋出的異常均可以標誌着線程的完成。

4 本地方法接口

  Java本地接口,也叫JNI(Java Native Interface),是爲可移植性準備的。本地方法接口容許本地方法完成如下工做:

  • 傳遞或返回數據
  • 操做實例變量
  • 操做類變量或調用類方法
  • 操做數組
  • 對堆的對象加鎖
  • 裝載新的類
  • 拋出異常
  • 捕獲本地方法調用Java方法拋出的異常
  • 捕獲虛擬機拋出的異步異常
  • 指示垃圾收集器某個對象再也不須要