1. 程式人生 > >jvm虛擬機器工作機制【轉】

jvm虛擬機器工作機制【轉】

轉自:https://www.cnblogs.com/lao-liang/p/5110710.html

1 概述

  眾所周知,Java支援平臺無關性、安全性和網路移動性。而Java平臺由Java虛擬機器和Java核心類所構成,它為純Java程式提供了統一的程式設計介面,而不管下層作業系統是什麼。正是得益於Java虛擬機器,它號稱的“一次編譯,到處執行”才能有所保障。

1.1 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中只有程式執行時需要的類才會被裝載,位元組碼由執行引擎來執行。

  當Java虛擬機器由主機作業系統上的軟體實現時,Java程式通過呼叫本地方法和主機進行互動。Java方法由Java語言編寫,編譯成位元組碼,儲存在class檔案中。本地方法由C/C++/組合語言編寫,編譯成和處理器相關的機器程式碼,儲存在動態連結庫中,格式是各個平臺專有。所以本地方法是聯絡Java程式和底層主機作業系統的連線方式。

  由於Java虛擬機器並不知道某個class檔案是如何被建立的,是否被篡改一無所知,所以它實現了一個class檔案檢測器,確保class檔案中定義的型別可以安全地使用。class檔案檢驗器通過四趟獨立的掃描來保證程式的健壯性:

  • class檔案的結構檢查
  • 型別資料的語義檢查
  • 位元組碼驗證
  • 符號引用驗證

  Java虛擬機器在執行位元組碼時還進行其它的一些內建的安全機制的操作,他們作為Java程式語言保證Java程式健壯性的特性,同時也是Java虛擬機器的特性:

  • 型別安全的引用轉換
  • 結構化的記憶體訪問
  • 自動垃圾收集
  • 陣列邊界檢查
  • 空引用檢查

1.3 Java虛擬機器資料型別

  Java虛擬機器通過某些資料型別來執行計算。資料型別可以分為兩種:基本型別和引用型別,如下圖:

  但boolean有點特別,當編譯器把Java原始碼編譯為位元組碼時,它會用int或byte表示boolean。在Java虛擬機器中,false是由0表示,而true則由所有非零整數表示。和Java語言一樣,Java虛擬機器的基本型別的值域在任何地方都是一致的,不管主機平臺是什麼,一個long在任何虛擬機器中總是一個64位二進位制補碼的有符號整數。

  對於returnAddress,這個基本型別被用來實現Java程式中的finally子句,Java程式設計師不能使用這個型別,它的值指向一條虛擬機器指令的操作碼。

2 體系結構

  在 Java虛擬機器規範中,一個虛擬機器例項的行為是分別按照子系統、記憶體區、資料型別和指令來描述的,這些組成部分一起展示了抽象的虛擬機器的內部體系結構。

2.1 class檔案

  Java class檔案包含了關於類或介面的所有資訊。class檔案的“基本型別”如下:

u1 1個位元組,無符號型別
u2 2個位元組,無符號型別
u4 4個位元組,無符號型別
u8 8個位元組,無符號型別

  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程式的一部分。(關於ClassLoader的工作機制請參考本人轉載的另一篇blog:http://blog.csdn.net/qq_31493821/article/details/78812057)

  • 啟動類裝載器(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方法丟擲的異常
  • 捕獲虛擬機器丟擲的非同步異常
  • 指示垃圾收集器某個物件不再需要

參考:

《深入Java虛擬機器》