深入理解Java虛擬機06--虛擬機字節(jié)碼執(zhí)行引擎
一.前言
物理機的執(zhí)行引擎是直接在物理硬件如CPU、操作系統(tǒng)、指令集上運行的,但是對于虛擬機來講,他的執(zhí)行引擎由自己實現(xiàn)。 執(zhí)行引擎有統(tǒng)一的外觀(Java虛擬機規(guī)范),不同類型的虛擬機都遵循了這一規(guī)范,輸入字節(jié)碼文件,解析字節(jié)碼處理,然后輸出結(jié)果。
二.運行時棧幀結(jié)構(gòu)

1、棧幀概念
棧幀(Stack Frame)用于支持方法調(diào)用和執(zhí)行的數(shù)據(jù)結(jié)構(gòu),包含了局部變量表、操作數(shù)棧、動態(tài)連接和方法返回地址。
- 局部變量表大小(max_locals),棧幀深度在編譯時已經(jīng)確定,并寫入到了Code屬性中;
- 執(zhí)行引擎運行的所有字節(jié)碼指令都只針對當(dāng)前棧進(jìn)行操作;
2、局部變量表
局部變量表存儲了方法參數(shù)以及方法內(nèi)定義的局部變量。
- Slot(變量槽):局部變量表容量最小單位,可以存放32位以內(nèi)的數(shù)據(jù)類型;
- refrence:
- 直接或者間接找到到該對象在“堆內(nèi)存”中數(shù)據(jù)存放的起始地址索引;
- 直接或者間接找到對象所屬數(shù)據(jù)類型在方法區(qū)中存儲的類型信息;
- 局部變量表建立在線程的堆棧上,所以操作兩個連續(xù)的slot是否為原子操作,都不會引起數(shù)據(jù)安全問題,但是如果是64位的話,不允許任何方式單獨訪問其中的一個;
- this:實例方法(非static)默認(rèn)第一個(第0位索引)slot為當(dāng)前對象自己的引用;
- slot重用:
- 當(dāng)前字節(jié)碼的pc計數(shù)器超出某個變量的作用域,那這個變量的slot可以交給別的變量使用;
- 影響到正常的Java垃圾回收機制;
- 賦null:因為上述slot重用的原因,當(dāng)方法域內(nèi)前面有局部變量定義了大內(nèi)存實際不再使用的變量,緊接著后面的代碼又是一個耗時的操作,這個時候及時賦null就顯得有大的意義。因為一旦觸發(fā)后,這部分的slot就可以被重用了。看起來就像是方法區(qū)內(nèi)部進(jìn)行“類gc"操作一樣。但是,并不是任何時候都要進(jìn)行賦null.以恰當(dāng)?shù)淖兞孔饔糜騺砜刂谱兞炕厥諘r間才是最優(yōu)雅的方式,并且賦null值操作在經(jīng)過JIT編譯優(yōu)化后會被消除掉,這樣的話實際是沒有任何意義的。
- 初始值:和類變量不同,局部變量系統(tǒng)不會自動賦初始值,所以沒有賦值是無法使用的,編譯都無法通過。即使通過,字節(jié)碼校驗階段也會檢查出來而導(dǎo)致類加載失??;
3、操作數(shù)棧(Operand Stack)
- 操作棧,后入先出;
- 最大深度:Code屬性表中的max_stacks;
- 32位數(shù)據(jù)類型所占棧容量為1,64位所占容量為2;
- 棧元素的數(shù)據(jù)類型必須和棧指令保持一致
- 兩個棧幀之間可以存在一部分的重疊,共享數(shù)據(jù),這樣在方法調(diào)用的時候避免的額外的參數(shù)復(fù)制。
- Java虛擬機的解釋執(zhí)行引擎也是:基于棧的執(zhí)行引擎;
4、動態(tài)連接(Dynamic Linking)
字節(jié)碼中的方法的調(diào)用都是通過常量池中指定方法的符號作為參數(shù)
- 靜態(tài)解析:這種符號有的是類加載階段或者首次使用初始化的時候轉(zhuǎn)化為直接的引用
- 動態(tài)連接:另外一部分是在運行時轉(zhuǎn)化為直接引用
5、方法返回地址
- 退出:
- 正常退出:遇到返回的字節(jié)碼指令;
- 異常退出:本方法異常表中沒有匹配的異常;
- 退出后,恢復(fù)上層方法的局部變量表和操作棧,有返回值就把返回值壓入上層調(diào)用者的棧中;
三.方法調(diào)用
定義:確定被調(diào)用方法的版本
1、解析
- 編譯器可知,運行期不可變。這類方法的調(diào)用成為解析,在類加載階段進(jìn)行解析。
- 靜態(tài)方法、私有方法、實例構(gòu)造器方法、父類方法,符合上述條件。特點是:
- 只能被invokestatic和invokespecial指令調(diào)用
- 不可繼承或者重寫,編譯時已經(jīng)確定了一個版本。
- 在類加載時會把符合引用解析為該方法的直接引用。
- 非虛方法(注意final也是非虛方法,其他的都是虛方法)
2、靜態(tài)分派
- 概念:根據(jù)靜態(tài)類型來定位方法的執(zhí)行版本
- 典型代表:方法的重載(方法名相同,參數(shù)類型不同)
- 發(fā)生時間:編譯階段
3、動態(tài)分派
- 概念:調(diào)用invokevirtual時,把常量池中的類方法符號解析到了不同的直接引用上。
- 典型代表:重寫,多態(tài)的重要體現(xiàn)
- 過程:
- 執(zhí)行invokevitual指令
- 在虛方法表(類加載階段,類變量初始化結(jié)束后會初始化虛方法表)中查找方法,沒有向上的父類進(jìn)行查找
- 方法宗量:方法的接收者與方法參數(shù)的總稱
- 單分派和多分派:
- 只有一個宗量作為方法的選擇依據(jù),稱為單分派。多個,則稱為多分派。
- 當(dāng)前的Java是靜態(tài)多分派、動態(tài)單分派的語言;
四.動態(tài)語言支持
-
特點:變量無類型,變量的值才有類型
-
invoke包:Java實現(xiàn)動態(tài)語言新增的包
五.指令集
- 基于棧的指令集
- 過程:入棧、計算、出棧
- 優(yōu)點:
- 可移植性,不依賴于硬件
- 代碼緊湊
- 缺點:
- 速度較慢
- 產(chǎn)生相當(dāng)多的指令數(shù)量
- 頻繁內(nèi)存訪問
- 基于寄存器的指令集
- 代表:x86
六.方法內(nèi)聯(lián)
- 方法內(nèi)聯(lián)的方式是通過吧“目標(biāo)方法”的代碼復(fù)制到發(fā)起調(diào)用的方法內(nèi),避免真實的方法調(diào)用。
- 內(nèi)聯(lián)消除了方法調(diào)用的成本,還為其他優(yōu)化手段建立良好的基礎(chǔ)。
- 編譯器在進(jìn)行內(nèi)聯(lián)時,如果是非虛方法,那么直接內(nèi)聯(lián)。如果遇到虛方法,則會查詢當(dāng)前程序下是否有多個目標(biāo)版本可供選擇,如果查詢結(jié)果只有一個版本,那么也可以內(nèi)聯(lián),不過這種內(nèi)聯(lián)屬于激進(jìn)優(yōu)化,需要預(yù)留一個逃生門(Guard條件不成立時的Slow Path),稱為守護(hù)內(nèi)聯(lián)。
- 如果程序的后續(xù)執(zhí)行過程中,虛擬機一直沒有加載到會令這個方法的接受者的繼承關(guān)系發(fā)現(xiàn)變化的類,那么內(nèi)聯(lián)優(yōu)化的代碼可以一直使用。否則需要拋棄掉已經(jīng)編譯的代碼,退回到解釋狀態(tài)執(zhí)行,或者重新進(jìn)行編譯
七.逃逸分析
逃逸分析的基本行為就是分析對象動態(tài)作用域:當(dāng)一個對象在方法里面被定義后,它可能被外部方法所引用,這種行為被稱為方法逃逸。被外部線程訪問到,被稱為線程逃逸。
如果對象不會逃逸到方法或線程外,可以做什么優(yōu)化?
- 棧上分配:一般對象都是分配在Java堆中的,對于各個線程都是共享和可見的,只要持有這個對象的引用,就可以訪問堆中存儲的對象數(shù)據(jù)。但是垃圾回收和整理都會耗時,如果一個對象不會逃逸出方法,可以讓這個對象在棧上分配內(nèi)存,對象所占用的內(nèi)存空間就可以隨著棧幀出棧而銷毀。如果能使用棧上分配,那大量的對象會隨著方法的結(jié)束而自動銷毀,垃圾回收的壓力會小很多。
- 同步消除:線程同步本身就是很耗時的過程。如果逃逸分析能確定一個變量不會逃逸出線程,那這個變量的讀寫肯定就不會有競爭,同步措施就可以消除掉。
- 標(biāo)量替換:不創(chuàng)建這個對象,直接創(chuàng)建它的若干個被這個方法使用到的成員變量來替換。
八.小結(jié)
在前面我們已經(jīng)了解到棧幀、方法區(qū)的內(nèi)存時線程私有的,本篇更加詳細(xì)的講了方法是怎么找到并執(zhí)行的。Java虛擬機規(guī)范:輸入字節(jié)碼,解析字節(jié)碼處理,輸出結(jié)果。首先,棧幀包含了局部變量表、操作數(shù)棧、動態(tài)連接、方法返回地址。字節(jié)碼中的方法都是通過常量池中的符號作為參數(shù)指定的,有些編譯解析確定,有些運行行時轉(zhuǎn)化為直接引用。首先記住,JVM是基于棧的執(zhí)行引擎。棧有著先入后出的特點,執(zhí)行引擎的指令也僅執(zhí)行當(dāng)前棧。而局部變量表存儲了方法內(nèi)需要的變量信息,是以Slot 為單位進(jìn)行存儲,超出操作域后,原本占用的內(nèi)存區(qū)域可以被其他的局部變量使用,類似“回收”。然后,記住Java是靜態(tài)多分派,動態(tài)單分派的語言。靜態(tài)分派,如方法的重載。通過方法的參數(shù)不同就可以確定要調(diào)用哪個方法,這個再編譯階段就定好。動態(tài)分派,如方法的重寫。執(zhí)行方法時,有一個虛方法表。這這個表里搜索,自己有就執(zhí)行自己的,沒有向上找父類的。這個是Java實現(xiàn)多態(tài)的重要原理。Java也有支持動態(tài)語言的invoke包,平時用的較少。
浙公網(wǎng)安備 33010602011771號