深入理解jvm(四):虛擬機器位元組碼執行引擎
執行時棧幀
每一個方法從呼叫開始到執行完成都對應著一張棧幀的進棧和出棧。棧幀中儲存著區域性變量表,運算元表,動態連結和方法返回地址。位於虛擬機器最頂層的稱為當前方法棧。
區域性變量表
儲存當前方法的區域性變數和引數,區域性變量表的容量以變數槽slot(32位)為儲存單位。對於64位的資料,通過連續分配兩個slot高位對齊的方式儲存,由於區域性變量表表是執行緒私有的,所以連續讀取兩個slot不會引起安全問題。
區域性變量表的計數從1開始,第0位預設為“this”指標。
為了節省棧空間,區域性變數的slot是可重用的。當某個slot的作用域沒有覆蓋全部的方法體,且程式計數器已經超出了該slot的作用域,則該slot可以被複用。
如果一個無用的區域性變數佔用了大量記憶體,那麼設定一個a=0;的可以讓垃圾回收器將其立即回收,這一點可以在某些極端情況下,用作物件記憶體過大,方法棧幀長時間不能回收,和方法呼叫次數倒不大jit的編譯水平時的奇技。
區域性變數和類屬性不同。類屬性存在一個準備階段,即類屬性在遇到一個new操作時,首先會檢查這個類有沒有被載入,如果載入完成,就通過“指標碰撞”或者“空閒列表法”的方式為其在堆中分配記憶體,為了解決指標碰撞可能產生的執行緒不安全問題,堆中會為每一個物件預留一塊tlab,只有tlab滿要使用其他區域的記憶體時才需要通過其他方式來保證執行緒安全。例如上鎖。那麼在前述的tlab或者分配記憶體的過程中,虛擬機器會為所有變數附上預設值,但是,區域性變數是無法享受這樣的待遇的,所以區域性變數不賦初值會導致編譯不通過。
運算元棧
存放區域性變數運算過程中的臨時變數,初始位0,可以另一個棧幀的區域性變量表共享資料以提高效率。
動態連結和方法返回地址
動態連結支援方法呼叫,可以分為靜態解析和動態連結
方法返回,相當於當前方法的棧幀出棧。執行的操作有:恢復上層棧幀的區域性變數和運算元棧,將返回值賦值到上層運算元棧中,調整pc計數器指向後一條指令。
方法呼叫
方法呼叫不等同於方法執行。方法呼叫是程式執行中最普遍最頻繁的操作,由於Class檔案中的方法調動依靠的是符號引用(即只是抽象層面的引用,被沒有指向實際呼叫方法的記憶體地址),所以將符號引用轉為直接引用的過程實際要到載入其中,甚至執行期間才能被確定下來。這為java語言的動態實現提供了極大的靈活性,同時也增加了程式設計的複雜性
解析
解析階段會將一部分符號引用轉化為直接引用。轉化的前提是:方法在編譯期間就有一個確定的呼叫版本,且在執行期間不會改變。這類的方法可以統稱為非虛方法,包括:靜態方法,私有方法,例項構造器和父類方法,以及final修飾的方法。
分派
分派的呼叫可能是靜態的也可能是動態的,根據分派依據的宗量數可以分為:靜態單分派,動態單分派,靜態多分派和動態多分派。
靜態分派
過載!所有依賴靜態型別來定位方法執行版本的分派動作成為靜態分派。靜態的典型應用是過載,靜態分派的型別在編譯階段就已經確定下來了。此外,還要理解編譯器確定一個更合適的版本時的模糊判斷(例如:char-int-long-float-double-包裝類-序列化-object)。
動態分派
重寫!假設一個類A被兩個子類BC重寫了方法m,則虛擬機器的執行過程為:將BC兩個引用(也成為接受者)壓入棧,然後分別呼叫invokevirtual指令來呼叫方法。其中,在invokevirtual執行的過程中,第一步就是查詢運算元棧頂第一個元素的型別,由於兩次的運算元棧頂分別是AB,所以invokevirtual呼叫的方法就是被重寫了的方法。這就是重寫的本質
單分派與多分派
宗量:方法接受者和方法引數的總稱。單分派根據一個宗量對目標方法進行選擇,多宗量根據多個宗量對目標方法進行選擇。在jdk1.7以前java是一門靜態多宗量,動態單宗量的語言。
基於棧的指令集和基於暫存器的指令集
基於暫存器的指令集執行速度更快。
基於棧的指令集可移植性更好,程式碼更緊湊,但是由於大量進出棧指令的原因,效率相對基於暫存器的指令集也更低。