(點擊上方公眾號,可快速關(guān)注) 1 概述 眾所周知,,Java支持平臺無關(guān)性,、安全性和網(wǎng)絡移動性。而Java平臺由Java虛擬機和Java核心類所構(gòu)成,,它為純Java程序提供了統(tǒng)一的編程接口,,而不管下層操作系統(tǒng)是什么。正是得益于Java虛擬機,,它號稱的“一次編譯,,到處運行”才能有所保障。 1.1 Java程序執(zhí)行流程Java程序的執(zhí)行依賴于編譯環(huán)境和運行環(huán)境,。源碼代碼轉(zhuǎn)變成可執(zhí)行的機器代碼,由下面的流程完成: Java技術(shù)的核心就是Java虛擬機,,因為所有的Java程序都在虛擬機上運行,。Java程序的運行需要Java虛擬機、Java API和Java Class文件的配合,。Java虛擬機實例負責運行一個Java程序,。當啟動一個Java程序時,一個虛擬機實例就誕生了,。當程序結(jié)束,,這個虛擬機實例也就消亡。 Java的跨平臺特性,,因為它有針對不同平臺的虛擬機,。 1.2 Java虛擬機Java虛擬機的主要任務是裝載class文件并且執(zhí)行其中的字節(jié)碼。由下圖可以看出,,Java虛擬機包含一個類裝載器(class loader),,它可以從程序和API中裝載class文件,Java API中只有程序執(zhí)行時需要的類才會被裝載,,字節(jié)碼由執(zhí)行引擎來執(zhí)行,。 當Java虛擬機由主機操作系統(tǒng)上的軟件實現(xiàn)時,Java程序通過調(diào)用本地方法和主機進行交互,。Java方法由Java語言編寫,,編譯成字節(jié)碼,,存儲在class文件中。本地方法由C/C++/匯編語言編寫,,編譯成和處理器相關(guān)的機器代碼,,存儲在動態(tài)鏈接庫中,格式是各個平臺專有,。所以本地方法是聯(lián)系Java程序和底層主機操作系統(tǒng)的連接方式,。 由于Java虛擬機并不知道某個class文件是如何被創(chuàng)建的,是否被篡改一無所知,,所以它實現(xiàn)了一個class文件檢測器,,確保class文件中定義的類型可以安全地使用。class文件檢驗器通過四趟獨立的掃描來保證程序的健壯性: Java虛擬機在執(zhí)行字節(jié)碼時還進行其它的一些內(nèi)置的安全機制的操作,,他們作為Java編程語言保證Java程序健壯性的特性,,同時也是Java虛擬機的特性: 1.3 Java虛擬機數(shù)據(jù)類型Java虛擬機通過某些數(shù)據(jù)類型來執(zhí)行計算。數(shù)據(jù)類型可以分為兩種:基本類型和引用類型,,如下圖: 但boolean有點特別,,當編譯器把Java源碼編譯為字節(jié)碼時,它會用int或byte表示boolean,。在Java虛擬機中,,false是由0表示,而true則由所有非零整數(shù)表示,。和Java語言一樣,,Java虛擬機的基本類型的值域在任何地方都是一致的,不管主機平臺是什么,,一個long在任何虛擬機中總是一個64位二進制補碼的有符號整數(shù),。 對于returnAddress,這個基本類型被用來實現(xiàn)Java程序中的finally子句,,Java程序員不能使用這個類型,,它的值指向一條虛擬機指令的操作碼。 2 體系結(jié)構(gòu)在 Java虛擬機規(guī)范中,,一個虛擬機實例的行為是分別按照子系統(tǒng),、內(nèi)存區(qū)、數(shù)據(jù)類型和指令來描述的,,這些組成部分一起展示了抽象的虛擬機的內(nèi)部體系結(jié)構(gòu),。 2.1 class文件Java class文件包含了關(guān)于類或接口的所有信息。class文件的“基本類型”如下:
如果想了解更多,,Oracle的JVM SE7給出了官方規(guī)范:The Java? Virtual Machine Specification class文件包含的內(nèi)容: ClassFile {
u4 magic; //魔數(shù):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; //接口索引計數(shù)器 u2 interfaces[interfaces_count]; //接口索引集合 u2 fields_count; //字段數(shù)量計數(shù)器 field_info fields[fields_count]; //字段表集合 u2 methods_count; //方法數(shù)量計數(shù)器 method_info methods[methods_count]; //方法表集合 u2 attributes_count; //屬性個數(shù) attribute_info attributes[attributes_count]; //屬性表
} 2.2 類裝載器子系統(tǒng)類裝載器子系統(tǒng)負責查找并裝載類型信息。其實Java虛擬機有兩種類裝載器:系統(tǒng)裝載器和用戶自定義裝載器,。前者是Java虛擬機實現(xiàn)的一部分,,后者則是Java程序的一部分。
除了系統(tǒng)提供的類裝載器以外,開發(fā)人員可以通過繼承 java.lang.ClassLoader類的方式實現(xiàn)自己的類裝載器,,以滿足一些特殊的需求,。 類裝載器子系統(tǒng)涉及Java虛擬機的其它幾個組成部分以及來自java.lang庫的類。ClassLoader定義的方法為程序提供了訪問類裝載器機制的接口,。此外,,對于每一個被裝載的類型,Java虛擬機都會為它創(chuàng)建一個java.lang.Class類的實例來代表該類型,。和其它對象一樣,用戶自定義的類裝載器以及Class類的實例放在內(nèi)存中的堆區(qū),,而裝載的類型信息則位于方法區(qū),。 類裝載器子系統(tǒng)除了要定位和導入二進制class文件外,還必須負責驗證被導入類的正確性,,為類變量分配并初始化內(nèi)存,,以及解析符號引用。這些動作還需要按照以下順序進行:
在Java虛擬機中,,關(guān)于被裝載的類型信息存儲在一個方法區(qū)的內(nèi)存中,。當虛擬機裝載某個類型時,它使用類裝載器定位相應的class文件,,然后讀入這個class文件并將它傳輸?shù)教摂M機中,,接著虛擬機提取其中的類型信息,,并將這些信息存儲到方法區(qū)。方法區(qū)也可以被垃圾回收器收集,,因為虛擬機允許通過用戶定義的類裝載器來動態(tài)擴展Java程序,。 方法區(qū)中存放了以下信息:
Java程序在運行時創(chuàng)建的所有類實例或數(shù)組(數(shù)組在Java虛擬機中是一個真正的對象)都放在同一個堆中。由于Java虛擬機實例只有一個堆空間,,所以所有線程都將共享這個堆,。需要注意的是,Java虛擬機有一條在堆中分配對象的指令,,卻沒有釋放內(nèi)存的指令,,因為虛擬機把這個任務交給垃圾收集器處理。Java虛擬機規(guī)范并沒有強制規(guī)定垃圾收集器,,它只要求虛擬機實現(xiàn)必須“以某種方式”管理自己的堆空間,。比如某個實現(xiàn)可能只有固定大小的堆空間,當空間填滿,,它就簡單拋出OutOfMemory異常,,根本不考慮回收垃圾對象的問題,但卻是符合規(guī)范的,。 Java虛擬機規(guī)范并沒有規(guī)定Java對象在堆中如何表示,,這給虛擬機的實現(xiàn)者決定怎么設計。一個可能的堆設計如下: 一個句柄池,,一個對象池,。一個對象的引用就是一個指向句柄池的本地指針。這種設計的好處有利于堆碎片的整理,,當移動對象池中的對象時,,句柄部分只需更改一下指針指向?qū)ο蟮男碌刂芳纯伞H秉c是每次訪問對象的實例變量都要經(jīng)過兩次指針傳遞,。 2.5 Java棧每當啟動給一個線程時,,Java虛擬機會為它分配一個Java棧,。Java棧由許多棧幀組成,一個棧幀包含一個Java方法調(diào)用的狀態(tài),。當線程調(diào)用一個Java方法時,,虛擬機壓入一個新的棧幀到該線程的Java棧中,當該方法返回時,,這個棧幀就從Java棧中彈出,。Java棧存儲線程中Java方法調(diào)用的狀態(tài)–包括局部變量、參數(shù),、返回值以及運算的中間結(jié)果等,。Java虛擬機沒有寄存器,其指令集使用Java棧來存儲中間數(shù)據(jù),。這樣設計的原因是為了保持Java虛擬機的指令集盡量緊湊,,同時也便于Java虛擬機在只有很少通用寄存器的平臺上實現(xiàn)。另外,,基于棧的體系結(jié)構(gòu),,也有助于運行時某些虛擬機實現(xiàn)的動態(tài)編譯器和即時編譯器的代碼優(yōu)化。 2.5.1 棧幀棧幀由局部變量區(qū),、操作數(shù)棧和幀數(shù)據(jù)區(qū)組成,。當虛擬機調(diào)用一個Java方法時,它從對應類的類型信息中得到此方法的局部變量區(qū)和操作數(shù)棧的大小,,并根據(jù)此分配棧幀內(nèi)存,,然后壓入Java棧中。 2.5.1.1 局部變量區(qū)局部變量區(qū)被組織為以字長為單位,、從0開始計數(shù)的數(shù)組,。字節(jié)碼指令通過從0開始的索引使用其中的數(shù)據(jù)。類型為int, float, reference和returnAddress的值在數(shù)組中占據(jù)一項,,而類型為byte, short和char的值在存入數(shù)組前都被轉(zhuǎn)換為int值,,也占據(jù)一項。但類型為long和double的值在數(shù)組中卻占據(jù)連續(xù)的兩項,。 2.5.1.2 操作數(shù)棧和局部變量區(qū)一樣,操作數(shù)棧也是被組織成一個以字長為單位的數(shù)組,。它通過標準的棧操作訪問–壓棧和出棧,。由于程序計數(shù)器無法被程序指令直接訪問,Java虛擬機的指令是從操作數(shù)棧中取得操作數(shù),,所以它的運行方式是基于棧而不是基于寄存器,。虛擬機把操作數(shù)棧作為它的工作區(qū),因為大多數(shù)指令都要從這里彈出數(shù)據(jù),,執(zhí)行運算,,然后把結(jié)果壓回操作數(shù)棧,。 2.5.1.3 幀數(shù)據(jù)區(qū)除了局部變量區(qū)和操作數(shù)棧,Java棧幀還需要幀數(shù)據(jù)區(qū)來支持常量池解析,、正常方法返回以及異常派發(fā)機制,。每當虛擬機要執(zhí)行某個需要用到常量池數(shù)據(jù)的指令時,它會通過幀數(shù)據(jù)區(qū)中指向常量池的指針來訪問它,。除了常量池的解析外,,幀數(shù)據(jù)區(qū)還要幫助虛擬機處理Java方法的正常結(jié)束或異常中止。如果通過return正常結(jié)束,,虛擬機必須恢復發(fā)起調(diào)用的方法的棧幀,,包括設置程序計數(shù)器指向發(fā)起調(diào)用方法的下一個指令;如果方法有返回值,,虛擬機需要將它壓入到發(fā)起調(diào)用的方法的操作數(shù)棧,。為了處理Java方法執(zhí)行期間的異常退出情況,幀數(shù)據(jù)區(qū)還保存一個對此方法異常表的引用,。 2.6 程序計數(shù)器對于一個運行中的Java程序而言,,每一個線程都有它的程序計數(shù)器。程序計數(shù)器也叫PC寄存器,。程序計數(shù)器既能持有一個本地指針,,也能持有一個returnAddress。當線程執(zhí)行某個Java方法時,,程序計數(shù)器的值總是下一條被執(zhí)行指令的地址,。這里的地址可以是一個本地指針,也可以是方法字節(jié)碼中相對該方法起始指令的偏移量,。如果該線程正在執(zhí)行一個本地方法,,那么此時程序計數(shù)器的值是“undefined”。 2.7 本地方法棧任何本地方法接口都會使用某種本地方法棧,。當線程調(diào)用Java方法時,,虛擬機會創(chuàng)建一個新的棧幀并壓入Java棧。當它調(diào)用的是本地方法時,,虛擬機會保持Java棧不變,,不再在線程的Java棧中壓入新的棧,虛擬機只是簡單地動態(tài)連接并直接調(diào)用指定的本地方法,。 其中方法區(qū)和堆由該虛擬機實例中所有線程共享,。當虛擬機裝載一個class文件時,它會從這個class文件包含的二進制數(shù)據(jù)中解析類型信息,,然后把這些類型信息放到方法區(qū),。當程序運行時,虛擬機會把所有該程序在運行時創(chuàng)建的對象放到堆中。 像其它運行時內(nèi)存區(qū)一樣,,本地方法棧占用的內(nèi)存區(qū)可以根據(jù)需要動態(tài)擴展或收縮,。 3 執(zhí)行引擎在Java虛擬機規(guī)范中,執(zhí)行引擎的行為使用指令集定義,。實現(xiàn)執(zhí)行引擎的設計者將決定如何執(zhí)行字節(jié)碼,,實現(xiàn)可以采取解釋、即時編譯或直接使用芯片上的指令執(zhí)行,,還可以是它們的混合,。 執(zhí)行引擎可以理解成一個抽象的規(guī)范、一個具體的實現(xiàn)或一個正在運行的實例,。抽象規(guī)范使用指令集規(guī)定了執(zhí)行引擎的行為,。具體實現(xiàn)可能使用多種不同的技術(shù)–包括軟件方面、硬件方面或樹種技術(shù)的結(jié)合,。作為運行時實例的執(zhí)行引擎就是一個線程,。 運行中Java程序的每一個線程都是一個獨立的虛擬機執(zhí)行引擎的實例。從線程生命周期的開始到結(jié)束,,它要么在執(zhí)行字節(jié)碼,,要么執(zhí)行本地方法。 3.1 指令集方法的字節(jié)碼流由Java虛擬機的指令序列構(gòu)成,。每一條指令包含一個單字節(jié)的操作碼,,后面跟隨0個或多個操作數(shù)。操作碼表示需要執(zhí)行的操作,;操作數(shù)向Java虛擬機提供執(zhí)行操作碼需要的額外信息,。當虛擬機執(zhí)行一條指令時,可能使用當前常量池中的項,、當前幀的局部變量中的值或者位于當前幀操作數(shù)棧頂端的值,。 抽象的執(zhí)行引擎每次執(zhí)行一條字節(jié)碼指令。Java虛擬機中運行的程序的每個線程(執(zhí)行引擎實例)都執(zhí)行這個操作,。執(zhí)行引擎取得操作碼,,如果操作碼有操作數(shù),就取得它的操作數(shù),。它執(zhí)行操作碼和跟隨的操作數(shù)規(guī)定的動作,,然后再取得下一個操作碼。這個執(zhí)行字節(jié)碼的過程在線程完成前將一直持續(xù),,通過從它的初始方法返回,,或者沒有捕獲拋出的異常都可以標志著線程的完成。 4 本地方法接口Java本地接口,,也叫JNI(Java Native Interface),是為可移植性準備的。本地方法接口允許本地方法完成以下工作: 參考: 【今日微信公號推薦↓】 |
|