目錄
一、運(yùn)行時(shí)棧幀結(jié)構(gòu)
二、方法調(diào)用
三永部、方法執(zhí)行
一字管、運(yùn)行時(shí)棧幀結(jié)構(gòu)
棧幀是用于支持虛擬機(jī)進(jìn)行 方法調(diào)用 和 方法執(zhí)行 的數(shù)據(jù)結(jié)構(gòu)啰挪,它是虛擬機(jī)運(yùn)行時(shí)數(shù)據(jù)區(qū)中的虛擬機(jī)棧的棧元素。棧幀存儲(chǔ)了方法的局部變量表嘲叔、操作數(shù)棧亡呵、動(dòng)態(tài)鏈接和方法返回地址等信息。每一個(gè)方法從調(diào)用開始至執(zhí)行完成的過程硫戈,都對(duì)應(yīng)著一個(gè)棧幀在虛擬機(jī)棧里面從入棧到出棧的過程锰什。
每一個(gè)棧幀都包括了局部變量表、操作數(shù)棧丁逝、動(dòng)態(tài)鏈接汁胆、方法返回地址和一些額外的附加信息。在編譯程序代碼的時(shí)候霜幼,棧幀中需要 多大的局部變量表嫩码,多深的操作數(shù)棧 都已經(jīng)完全確定了,并且寫入到了方法表的 Code 屬性中罪既,因此一個(gè)棧幀需要分配多少 內(nèi)存铸题,不會(huì)受到程序運(yùn)行期變量數(shù)據(jù)的影響,而僅僅取決于具體的虛擬機(jī)實(shí)現(xiàn)琢感。
一個(gè)線程中的方法調(diào)用鏈可能很長(zhǎng)(如一個(gè)方法內(nèi)調(diào)用了許多其他方法)丢间,很多方法都同時(shí)處于執(zhí)行狀態(tài)。對(duì)于執(zhí)行引擎來(lái)說(shuō)猩谊,在活動(dòng)線程 中千劈,只有位于棧頂?shù)臈攀怯行У模Q為 當(dāng)前棧幀 牌捷,與這個(gè)棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法墙牌。執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令都只針對(duì)當(dāng)前棧幀進(jìn)行操作,在概念模型上暗甥,典型的棧幀結(jié)構(gòu)如下所示喜滨。
- 局部變量表
- 局部變量表是一組變量值存儲(chǔ)空間,用于存放 方法參數(shù) 和 方法內(nèi)部定義的局部變量辜膝。在 Java 程序編譯為 Class 文件時(shí)无牵,就在方法的 Code 屬性的 max_locals 數(shù)據(jù)項(xiàng)中確定了該方法所需要分配的局部變量表的最大容量。
- 局部變量表是以 變量槽(Variable Slot厂抖,下稱Slot)為最小單位茎毁,每個(gè) Slot 都應(yīng)該能存放 一個(gè)boolean、short忱辅、byte七蜘、char、int墙懂、float橡卤、reference 和 returnAddress 類型的數(shù)據(jù), 這 8 種數(shù)據(jù)類型损搬,都可以使用 32 位或更小的物理內(nèi)存來(lái)存放碧库。
- 一個(gè) Slot 可以存放一個(gè) 32 位以內(nèi)的數(shù)據(jù)類型,Java 中占用 32 位以內(nèi)的數(shù)據(jù)類型有 boolean场躯、short谈为、 byte、char踢关、int、float粘茄、reference 和 returnAddress 8 種類型签舞。前面 6 種不需要解釋,而第 7 種 reference 類型表示一個(gè)對(duì)象實(shí)例的引用柒瓣,虛擬機(jī)規(guī)范并對(duì)其特別說(shuō)明儒搭, 但虛擬機(jī)實(shí)現(xiàn)至少都應(yīng)當(dāng)能通過這個(gè)引用做到這兩點(diǎn):第一是從引用中直接或間接地查找到對(duì)象在 Java 堆中 數(shù)據(jù)存放的起始地址索引;第二是此引用中直接或間接地查找到對(duì)象所屬數(shù)據(jù)類型在方法區(qū)中的存儲(chǔ)的類型 信息芙贫,否則無(wú)法實(shí)現(xiàn) Java 語(yǔ)言規(guī)范中定義的語(yǔ)法約束搂鲫。第 8 種已經(jīng)很少見了,它是為字節(jié)碼指令 jsr磺平、jsr_w 和 ret 服務(wù)的魂仍,指向了一條字節(jié)碼指令的地址。
- 對(duì)于 64 位的數(shù)據(jù)類型拣挪,虛擬機(jī)會(huì)以高位對(duì)齊的方式為其分配兩個(gè)連續(xù)的 Slot 空間擦酌,Java 語(yǔ)言中明確的 64 位數(shù)據(jù)類型只有 long 和double 兩種。由于局部變量表建立在線程的堆棧上菠劝,是線程私有的數(shù)據(jù)赊舶, 無(wú)論讀寫兩個(gè)連續(xù)的 Slot 是否為原子操作,都不會(huì)引起數(shù)據(jù)安全問題。
- 在方法執(zhí)行時(shí)笼平,虛擬機(jī)是使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞過程的园骆,如果執(zhí)行的是實(shí)例方法(即非靜態(tài)方法),那局部變量表的第 0 個(gè)索引的 Slot 默認(rèn)是用于傳遞方法所屬對(duì)象實(shí)例的引用寓调, 在方法中可以通過關(guān)鍵字 “this” 來(lái)訪問到這個(gè)隱含的參數(shù)锌唾。其余參數(shù)則按照參數(shù)表順序排列,占用從 1 開始 的局部變量 Slot捶牢,參數(shù)表分配完畢后鸠珠,再根據(jù)方法體內(nèi)部定義的變量順序和作用域分配其余的 Slot。
- 操作數(shù)棧
- 操作數(shù)棧也常稱為操作棧秋麸,它是一個(gè)先進(jìn)后出的棧渐排。
- 同局部變量表一樣,操作數(shù)棧的最大深度也在編譯的時(shí)候?qū)懭氲?Code 屬性的 max_stacks 數(shù)據(jù)項(xiàng)中灸蟆。棧的最小深度為0驯耻,最大深度直到報(bào)棧溢出異常。
- 32 位數(shù)據(jù)類型所占的棧容量為 1炒考,64 位數(shù)據(jù)類型所占的棧容量為 2可缚。即 32 位的數(shù)據(jù)類型(boolean、short斋枢、 byte帘靡、char、int瓤帚、float描姚、reference 和 returnAddress)在棧中占 1 個(gè)空間,64 位數(shù)據(jù)類型(long戈次、double)在棧中占 2 個(gè)空間轩勘。
- 當(dāng)一個(gè)方法剛剛開始執(zhí)行的時(shí)候,這個(gè)方法的操作數(shù)棧是空的怯邪,在方法的執(zhí)行過程中绊寻,會(huì)有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是進(jìn)棧和出棧操作悬秉。
- 操作數(shù)棧中元素的數(shù)據(jù)類型必須與字節(jié)碼指令的序列嚴(yán)格匹配澄步,在編譯程序代碼的時(shí)候,編譯期要嚴(yán)格保證 這一點(diǎn)搂捧,在類校驗(yàn)階段的數(shù)據(jù)流分析中還要再次驗(yàn)證這一點(diǎn)驮俗。
- 動(dòng)態(tài)連接
- 每個(gè)棧幀都包含一個(gè)指向運(yùn)行時(shí)常量池中該棧幀所屬方法的引用, 持有這個(gè)引用是為支持方法調(diào)用過程中的動(dòng)態(tài)連接允跑。Class 文件的常量池存在 大量的符號(hào)引用王凑,字節(jié)碼中的方法調(diào)用指令就以常量池中指向方法的符號(hào)引用 作為參數(shù)搪柑。
- 符號(hào)引用轉(zhuǎn)換為直接引用可以分為 靜態(tài)解析 和 動(dòng)態(tài)連接。
靜態(tài)解析:存在 Class 文件的常量池中的符號(hào)引用索烹,這些符號(hào)引用一部分會(huì)在類加載或者第一次使用的時(shí)候 就轉(zhuǎn)化為直接引用工碾,這種轉(zhuǎn)化稱為靜態(tài)解析。如靜態(tài)方法和私有方法百姓,前者與類型直接關(guān)聯(lián)渊额,后者在外部不可 被訪問,這兩種方法各自的特定決定了他們都不可能通過繼承或別的方法重寫其他版本垒拢,因此它們都適合在類 加載階段進(jìn)行解析旬迹。
動(dòng)態(tài)連接:另外一部分符號(hào)引用將在每一次運(yùn)行期間轉(zhuǎn)換為直接引用,這部分成功為動(dòng)態(tài)連接求类。
-
方法返回地址
當(dāng)一個(gè)方法開始執(zhí)行后奔垦, 只有兩種方式可以退出這個(gè)方法。
- 正常完成出口:這種方式是執(zhí)行引擎遇到任意一個(gè)方法返回的字節(jié)碼指令尸疆, 這時(shí)候可能會(huì)有返回值傳遞給上層的方法調(diào)用者椿猎,是否有返回值和返回值的類 型將根據(jù)遇到何種方法返回指令來(lái)決定。
- 異常完成出口:這種退出方式是寿弱,在方法執(zhí)行過程中遇到了異常犯眠, 并且這個(gè)異常沒有在方法體內(nèi)得到處理,無(wú)論是 Java 虛擬機(jī)內(nèi)部產(chǎn)生的異常症革, 還是代碼中使用 athrow 字節(jié)碼指令產(chǎn)生的異常筐咧,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會(huì)導(dǎo)致方法按退出噪矛。
二嗜浮、方法調(diào)用
方法調(diào)用并不等同于方法執(zhí)行,方法調(diào)用階段唯一的任務(wù)就是確定 被調(diào)用方法的版本(即調(diào)用哪一個(gè)方法)摩疑, 暫時(shí)還不涉及到方法內(nèi)部的具體運(yùn)行過程。 在程序運(yùn)行時(shí)畏铆,進(jìn)行方法調(diào)用是最普遍雷袋、最 頻繁的操作,但是 Class 文件的編譯過程中不 包含傳統(tǒng)編譯中的連接步驟辞居,一切方法調(diào)用在 Class 文件里面存儲(chǔ)的都只是符號(hào)引用楷怒,而不是 方法在實(shí)際運(yùn)行時(shí)內(nèi)存布局中的入口地址(直接引用)。 這個(gè)特性給 Java 帶來(lái)了更強(qiáng)大的動(dòng)態(tài)擴(kuò)展能力瓦灶,但也使得 Java 方法調(diào)用過程變得相對(duì)復(fù)雜起來(lái)鸠删,需要在類加載期間, 甚至到運(yùn)行期間才能確定目標(biāo)方法的直接引用贼陶。
- 解析調(diào)用
- 所有方法調(diào)用的目標(biāo)方法在 Class 文件里面都是常量池中的符號(hào)引用刃泡, 在類加載的解析階段巧娱,會(huì)將其中一部分符號(hào)引用轉(zhuǎn)化為直接引用,這種解析能成立的前提是:方法在程序真正運(yùn)行之前就能有一個(gè) 可確定的版本烘贴,并且這個(gè)方法的調(diào)用版本 在運(yùn)行期是不可改變的禁添。換句話說(shuō),調(diào)用目標(biāo)在程序代碼寫好桨踪、編譯期進(jìn)行編譯 時(shí)就必須確定下來(lái)老翘。這類方法的調(diào)用稱為解析(Resolution)。
- 符合“解析”的方法:在Java語(yǔ)言中符號(hào)“編譯期可知锻离,運(yùn)行期不可變”這個(gè)要求的方法铺峭,主要包括 靜態(tài)方法 和 私有方法 兩大類,前者與類型直接關(guān)聯(lián)汽纠,后者在外部不可被訪問卫键,這兩種方法各自的特定決定了他們都不可能通過繼承或別的方法重寫其他版本,因此它們都適合在類加載階段進(jìn)行解析疏虫。
-
分派調(diào)用
眾所周知永罚,Java 是一門面向?qū)ο蟮某绦蛘Z(yǔ)言,因?yàn)?Java 具備面向?qū)ο蟮?3 個(gè)基本特征:繼承卧秘、封裝和多態(tài)呢袱。分派調(diào)用過程將會(huì)揭示多態(tài)性特征的一些最基本的體現(xiàn),如“重載”和“重寫” 在Java虛擬機(jī)之中是如何實(shí)現(xiàn)的翅敌。分派調(diào)用可以分為 “靜態(tài)分派” 和 “動(dòng)態(tài)分派”羞福。
- 靜態(tài)分派:所有依賴靜態(tài)類型(方法的參數(shù)類型)來(lái)定位方法執(zhí)行版本的分派動(dòng)作稱為靜態(tài)分派。靜態(tài)分派的典型應(yīng)用是方法的 重載蚯涮。靜態(tài)分派發(fā)生在編譯階段治专,因此確定靜態(tài)分派的動(dòng)作實(shí)際上不是由虛擬機(jī)來(lái)執(zhí)行的。
-
動(dòng)態(tài)分派:
在運(yùn)行期根據(jù)實(shí)際類型確定方法執(zhí)行版本的分派過程稱為動(dòng)態(tài)動(dòng)態(tài)遭顶。動(dòng)態(tài)分派的典型應(yīng)用是方法的 重寫张峰。動(dòng)態(tài)分派是通過字節(jié)碼 invokevirtual 指令進(jìn)行多態(tài)查找的過程的,而 invokevirtual 字節(jié)碼指令的作用是調(diào)用實(shí)例方法(這里指的是可被重寫的實(shí)例方法)棒旗,invokevirtual 指令的運(yùn)行時(shí)解析過程大致分為以下幾個(gè)步驟:
1)喘批、找到操作數(shù)棧頂?shù)牡谝粋€(gè)元素所指向的對(duì)象的實(shí)際類型,記作 C铣揉。
2)饶深、如果在類型 C 中找到與常量中的描述符和簡(jiǎn)單名稱都相符的方法,則進(jìn)行訪問權(quán)限校驗(yàn)逛拱,如果通過則返回這個(gè)方法的直接引用敌厘,查找結(jié)束;如果不通過朽合,則返回 java.lang.IllegalAccessError 異常俱两。
3)饱狂、否則,按照繼承關(guān)系 從下往上依次對(duì) C 的各個(gè)父類進(jìn)行第 2 步的搜索和驗(yàn)證過程锋华。
4)嗡官、如果始終沒有找到合適的方法,則拋出 Java.lang.AbstractMethodError 異常毯焕。
三衍腥、方法的執(zhí)行
上面我們了解了虛擬機(jī)是如何調(diào)用方法的,這里我們來(lái)探討一下虛擬機(jī)是如何執(zhí)行方法中的字節(jié)碼指令的纳猫。許多 Java 虛擬機(jī)的執(zhí)行引擎在執(zhí)行 Java 代碼的時(shí)候都有解析執(zhí)行(通過解析器執(zhí)行)和編譯執(zhí)行(通過即時(shí)編譯器產(chǎn)生本地代碼執(zhí)行)兩種選擇(即許多主流的商用虛擬機(jī)都同時(shí)包含解析器與編譯器)婆咸,這里主要說(shuō)明一下解析執(zhí)行。
1芜辕、基于棧的指令集和基于寄存器的指令集
Java 編譯器輸出的指令流尚骄,基本上是一種 基于棧的指令集架構(gòu),指令流中的指令大部分都是零地址指令侵续,它們依賴操作數(shù)棧進(jìn)行工作倔丈,與之相對(duì)的另一套常用的指令集架構(gòu)是 基于寄存器的指令集,最典型的就是 x86 的二地址指令集状蜗,換句話說(shuō)需五,就是現(xiàn)在我們主流 PC 機(jī)中直接支持的指令集架構(gòu),這些指令依賴寄存器進(jìn)行工作轧坎。那么棧的指令集與基于寄存器的指令集這兩者之間有什么不同呢宏邮?
舉個(gè)例子,分別使用兩種指令集計(jì)算 “1 + 1” 的結(jié)果缸血,基于棧的指令集回事這樣的:
iconst_1
iconst_1
iadd
istore_0
兩條 iconst_1 指令連續(xù)把兩個(gè)常量 1 壓入操作數(shù)棧中蜜氨,iadd 指令把棧頂?shù)膬蓚€(gè)值出棧、相加捎泻,然后把結(jié)果放回棧頂飒炎,最后 istore_0 把棧頂?shù)闹捣诺骄植孔兞勘淼牡?0 個(gè) Slot(變量槽) 中。
如果基于寄存器笆豁,那程序可能會(huì)是這個(gè)樣子:
mov eax, 1
add eax, 1
mov 指令把 EAX 寄存器的值設(shè)為 1厌丑,然后 add 指令再把這個(gè)值加 1,最后結(jié)果再保存到 EAX 寄存器中渔呵。
基于棧的指令集和基于寄存器的指令集比較:
- 基于棧的指令集的主要優(yōu)點(diǎn)是可移植,而寄存器由硬件直接提供砍鸠,程序直接依賴這些硬件寄存器則不可避免的要收到硬件的約束扩氢。
- 基于棧的指令集還有一些優(yōu)點(diǎn),如代碼更加緊湊爷辱、編譯器實(shí)現(xiàn)更加簡(jiǎn)單等录豺。
- 棧結(jié)構(gòu)指令集的主要缺點(diǎn)是執(zhí)行速度相對(duì)來(lái)說(shuō)會(huì)稍慢一點(diǎn)朦肘,因?yàn)閷?shí)現(xiàn)相同的功能,需要的指令數(shù)量一般會(huì)比寄存器指令集多双饥,因出棧媒抠、入棧操作本身就產(chǎn)生了相當(dāng)多的指令數(shù)量。
2咏花、基于棧的解析器執(zhí)行過程
這里通過一個(gè)例子來(lái)說(shuō)明一下趴生,代碼如下所示。