深刻理解Java虛擬機06--虛擬機字節碼執行引擎

一.前言安全

物理機的執行引擎是直接在物理硬件如CPU、操做系統、指令集上運行的,可是對於虛擬機來說,他的執行引擎由本身實現。 執行引擎有統一的外觀(Java虛擬機規範),不一樣類型的虛擬機都遵循了這一規範,輸入字節碼文件,解析字節碼處理,而後輸出結果。數據結構

二.運行時棧幀結構優化

一、棧幀概念this

棧幀(Stack Frame)用於支持方法調用和執行的數據結構,包含了局部變量表、操做數棧、動態鏈接和方法返回地址。操作系統

(1)局部變量表大小(max_locals),棧幀深度在編譯時已經肯定,並寫入到了Code屬性中;線程

(2)執行引擎運行的全部字節碼指令都只針對當前棧進行操做; 二、局部變量表cdn

局部變量表存儲了方法參數以及方法內定義的局部變量。對象

Slot(變量槽):局部變量表容量最小單位,能夠存放32位之內的數據類型; refrence: 直接或者間接找到到該對象在「堆內存」中數據存放的起始地址索引;blog

直接或者間接找到對象所屬數據類型在方法區中存儲的類型信息繼承

局部變量表創建在線程的堆棧上,因此操做兩個連續的slot是否爲原子操做,都不會引發數據安全問題,可是若是是64位的話,不容許任何方式單獨訪問其中的一個;

this:實例方法(非static)默認第一個(第0位索引)slot爲當前對象本身的引用;

slot重用:

當前字節碼的pc計數器超出某個變量的做用域,那這個變量的slot能夠交給別的變量使用;

影響到正常的Java垃圾回收機制;

賦null:由於上述slot重用的緣由,當方法域內前面有局部變量定義了大內存實際再也不使用的變量,緊接着後面的代碼又是一個耗時的操做,這個時候及時賦null就顯得有大的意義。由於一旦觸發後,這部分的slot就能夠被重用了。看起來就像是方法區內部進行「類gc"操做同樣。可是,並非任什麼時候候都要進行賦null.以恰當的變量做用域來控制變量回收時間纔是最優雅的方式,而且賦null值操做在通過JIT編譯優化後會被消除掉,這樣的話實際是沒有任何意義的。

初始值:和類變量不一樣,局部變量系統不會自動賦初始值,因此沒有賦值是沒法使用的,編譯都沒法經過。即便經過,字節碼校驗階段也會檢查出來而致使類加載失敗;

三、操做數棧(Operand Stack)

操做棧,後入先出;

最大深度:Code屬性表中的max_stacks;

32位數據類型所佔棧容量爲1,64位所佔容量爲2;

棧元素的數據類型必須和棧指令保持一致

兩個棧幀之間能夠存在一部分的重疊,共享數據,這樣在方法調用的時候避免的額外的參數複製。

Java虛擬機的解釋執行引擎也是:基於棧的執行引擎;

四、動態鏈接(Dynamic Linking)

字節碼中的方法的調用都是經過常量池中指定方法的符號做爲參數

靜態解析:這種符號有的是類加載階段或者首次使用初始化的時候轉化爲直接的引用

動態鏈接:另一部分是在運行時轉化爲直接引用

五、方法返回地址

退出:

正常退出:遇到返回的字節碼指令;

異常退出:本方法異常表中沒有匹配的異常;

退出後,恢復上層方法的局部變量表和操做棧,有返回值就把返回值壓入上層調用者的棧中;

三.方法調用

定義:肯定被調用方法的版本

一、解析

編譯器可知,運行期不可變。這類方法的調用成爲解析,在類加載階段進行解析。

靜態方法、私有方法、實例構造器方法、父類方法,符合上述條件。特色是:

只能被invokestatic和invokespecial指令調用

不可繼承或者重寫,編譯時已經肯定了一個版本。

在類加載時會把符合引用解析爲該方法的直接引用。

非虛方法(注意final也是非虛方法,其餘的都是虛方法)

二、靜態分派

概念:根據靜態類型來定位方法的執行版本

典型表明:方法的重載(方法名相同,參數類型不一樣)

發生時間:編譯階段

三、動態分派

概念:調用invokevirtual時,把常量池中的類方法符號解析到了不一樣的直接引用上。

典型表明:重寫,多態的重要體現

過程:

執行invokevitual指令

在虛方法表(類加載階段,類變量初始化結束後會初始化虛方法表)中查找方法,沒有向上的父類進行查找

方法宗量:方法的接收者與方法參數的總稱

單分派和多分派:

只有一個宗量做爲方法的選擇依據,稱爲單分派。多個,則稱爲多分派。

當前的Java是靜態多分派、動態單分派的語言;

四.動態語言支持

特色:變量無類型,變量的值纔有類型

invoke包:Java實現動態語言新增的包

五.指令集

基於棧的指令集

過程:入棧、計算、出棧

優勢:

可移植性,不依賴於硬件

代碼緊湊

缺點:

速度較慢

產生至關多的指令數量

頻繁內存訪問

基於寄存器的指令集

表明:x86

六.方法內聯

方法內聯的方式是經過吧「目標方法」的代碼複製到發起調用的方法內,避免真實的方法調用。

內聯消除了方法調用的成本,還爲其餘優化手段創建良好的基礎。

編譯器在進行內聯時,若是是非虛方法,那麼直接內聯。若是遇到虛方法,則會查詢當前程序下是否有多個目標版本可供選擇,若是查詢結果只有一個版本,那麼也能夠內聯,不過這種內聯屬於激進優化,須要預留一個逃生門(Guard條件不成立時的Slow Path),稱爲守護內聯。

若是程序的後續執行過程當中,虛擬機一直沒有加載到會令這個方法的接受者的繼承關係發現變化的類,那麼內聯優化的代碼能夠一直使用。不然須要拋棄掉已經編譯的代碼,退回到解釋狀態執行,或者從新進行編譯

七.逃逸分析

逃逸分析的基本行爲就是分析對象動態做用域:當一個對象在方法裏面被定義後,它可能被外部方法所引用,這種行爲被稱爲方法逃逸。被外部線程訪問到,被稱爲線程逃逸。

若是對象不會逃逸到方法或線程外,能夠作什麼優化?

棧上分配:通常對象都是分配在Java堆中的,對於各個線程都是共享和可見的,只要持有這個對象的引用,就能夠訪問堆中存儲的對象數據。可是垃圾回收和整理都會耗時,若是一個對象不會逃逸出方法,可讓這個對象在棧上分配內存,對象所佔用的內存空間就能夠隨着棧幀出棧而銷燬。若是能使用棧上分配,那大量的對象會隨着方法的結束而自動銷燬,垃圾回收的壓力會小不少。

同步消除:線程同步自己就是很耗時的過程。若是逃逸分析能肯定一個變量不會逃逸出線程,那這個變量的讀寫確定就不會有競爭,同步措施就能夠消除掉。

標量替換:不建立這個對象,直接建立它的若干個被這個方法使用到的成員變量來替換。

八.小結

  在前面咱們已經瞭解到棧幀、方法區的內存時線程私有的,本篇更加詳細的講了方法是怎麼找到並執行的。Java虛擬機規範:輸入字節碼,解析字節碼處理,輸出結果。首先,棧幀包含了局部變量表、操做數棧、動態鏈接、方法返回地址。字節碼中的方法都是經過常量池中的符號做爲參數指定的,有些編譯解析肯定,有些運行行時轉化爲直接引用。首先記住,JVM是基於棧的執行引擎。棧有着先入後出的特色,執行引擎的指令也僅執行當前棧。而局部變量表存儲了方法內須要的變量信息,是以Slot 爲單位進行存儲,超出操做域後,本來佔用的內存區域能夠被其餘的局部變量使用,相似「回收」。而後,記住Java是靜態多分派,動態單分派的語言。靜態分派,如方法的重載。經過方法的參數不一樣就能夠肯定要調用哪一個方法,這個再編譯階段就定好。動態分派,如方法的重寫。執行方法時,有一個虛方法表。這這個表裏搜索,本身有就執行本身的,沒有向上找父類的。這個是Java實現多態的重要原理。Java也有支持動態語言的invoke包,平時用的較少。

相關文章
相關標籤/搜索