1. 程式人生 > >深入理解Java虛擬機器----(七)位元組碼執行引擎

深入理解Java虛擬機器----(七)位元組碼執行引擎

    位元組碼執行引擎是執行引擎是最重要的一部分,概念模型的總體外觀是一致的:輸入位元組碼,過程是位元組碼解析的等效過程,輸出結果。不同的虛擬機器有不同的具體實現,大體有解釋執行和編譯執行兩種選擇。 執行時棧結構:     棧楨在虛擬機器棧中,是支援方法呼叫和執行的結構。儲存區域性變量表、運算元棧、動態連結和方法返回地址等。在編譯時,需要多大的區域性表量表,多深的運算元棧都確定了,所以需要多大記憶體也確定了。只有處於棧頂部的“當前棧楨”,才是有效的。
  • 區域性變量表:存放方法引數和區域性變數。以變數槽存放資料,變數槽有索引,從0開始。0是當前物件的引用,用this關鍵字可以獲取。其餘是其他引數和區域性變數,虛擬機器通過索引取值。區域性變數不像類變數,不會賦初始值,沒賦值過就不能使用。
  • 運算元棧:方法進入時是空的,許多指令會不停的吧資料出入棧。算數指令會對棧內資料計算,寫回結果;方法呼叫會用運算元棧傳遞引數。
  • 動態連結:指向執行時常量池中,該棧楨所屬方法的引用。為了支援方法呼叫過程中的動態連結。
  • 方法返回地址:方法退出時,需要回到上層呼叫的位置。棧楨需要儲存這個地址。

方法呼叫:     方法的呼叫在class檔案裡,只是符號引用的形式。直到類載入階段甚至執行時,才能確定記憶體入口--直接引用。
  • 解析:在類載入的解析階段,將一部分的符號引用轉換為直接引用,前提是執行前有一個確定版本,而且不可變,這種叫解析。主要包括:靜態方法、私有方法、例項構造器、父類方法。這是一個靜態的過程。
  • 分派:分派可能是靜態,也可能是動態。根據宗量數可分為單分派和多分派。於是就有4種可能。
    • 靜態分派:靜態分派發生在編譯階段,典型應用是過載
      。過載是通過引數的外觀型別來分派的。下面這段程式碼會輸出什麼呢?答案是“h”。因為H是h的外觀型別或者叫靜態型別。靜態型別是編譯期可知的,而h的實際型別卻要使用時可知,因為執行時可變的。所以編譯期能確定的最接近的型別是H。根據這個型別,將第一個say方法的符號引用作為了呼叫方法指令invokevirtual的引數。
  • 動態分派:和重寫有很大關係。invokevirtual的過程大概是,找到找到運算元棧頂第一個元素的實際型別C。在C中查詢與常量中完全相符的方法,如果找不到就層層往上,查詢父類。這個過程第一步就是找實際型別,然後直接找方法執行。這就是重寫的本質。這種執行時動態的確定執行版本的過程,稱為動態分派。
  • 單分派、多分派:方法的接受者和引數,總稱為方法的宗量。單分派就是根據一個宗量對方法進行選擇。多分派當然就是根據多個宗量選擇。
  • 靜態分派需要根據靜態型別和引數確定,所以是多分派的。而動態分派已經確定了引數資訊,只需確定方法接收者的實際型別。所以動態分派是單分派。由此可見,java是一門靜態多分派、動態單分派的語言。
動態型別語言支援: 動態語言的特徵是,它的型別檢查過程是在執行期間,而不是編譯期。例如lua、Python。在編譯器檢查就是靜態語言,如c++、java。一門語言中每一種檢查要在編譯期檢查還是執行期檢查,沒有必然的邏輯因果關係,都是規範中人為規定的。在java中,編譯期酒將方法呼叫翻譯成了符號引用,這個符號引用包括了這個方法屬於哪個型別、方法名、引數等資訊;而在動態語言中變數是沒有型別的,只有變數的值才有型別,編譯時只能確定方法名、引數等,方法的接受者不固定,要在執行時才知道。     靜態更安全,編譯時發現問題,擴大程式碼規模。動態更靈活,程式碼精巧、提升效率。     JDK1.7以前的指令集,4種方法呼叫指令,第一個引數都是被呼叫方法的符號引用。但符號引用是在編譯期產生,而動態語言中方法的接受者在執行時才確定。這樣,在JVM上實現動態語言需要用其他複雜的方式。在JDK1.7中加入了invokedynamic指令和java.lang.invoke包。這兩種方式都是為了解決原有的呼叫指令規則固化到虛擬機器中的問題,將方法的查詢由虛擬機器交到了使用者程式碼手中。前者用位元組碼和類屬性、常量實現;後者用java API實現。     java.lang.invoke中提供了MehtodHandler,類似於對一個方法的引用,比如c++的函式指標,c#的delegate。有了這個引用,就可以實現對方法的呼叫。有人會疑問,這個通過反射不是早都實現了麼?他們之間還是有很大的差別:
  • 反射是模擬Java程式碼層面的方法呼叫,MehtodHandler是位元組碼層面的。MehtodHandler中的幾個find方法對應著幾個方法呼叫指令。
  • 反射中,Method包含了方法各種各樣的複雜資訊,是重量級的。而MehtodHandler只包含執行該方法的相關資訊,是輕量級的。
  • MehtodHandler是對位元組碼指令的模擬,虛擬機器的優化可以應用到上面,反射則不行。
  • 反射是為了Java服務的,而MehtodHandler是為了虛擬機器服務,可以支援虛擬機器上的多種語言。
   invokedynamic指令出現的位置叫動態呼叫點,這個指令的第一個引數不再是以前的符號引用,而是JDK1.7新加入的常量,包含:引導方法、方法型別、名稱。     一個例子,有祖父類A,包含方法F,父類B繼承A,重寫F,子類C繼承B,重寫F。如何在C的F方法中呼叫B和C中的F方法?B中的F方法可以通過super輕易呼叫,那A中的呢?單純的java程式碼是做不到的。可以用MehtodHandler實現。 基於棧的位元組碼解釋執行引擎     JVM在執行Java程式碼時,有兩種選擇:解釋執行、編譯執行。 解釋執行:          現代基於虛擬機器的高階語言,編譯過程大多如上圖。java中,javac編譯器完成了詞法分析、語法分析到抽象語法樹,再遍歷樹生成線性指令流。這部分是在虛擬機器外進行的,而直譯器在虛擬機器內部。編譯器輸出的指令流是一種基於棧的指令集架構,依賴運算元棧工作。主流pc指令集是依賴暫存器工作。基於棧的好處是不直接依賴硬體,可以執行好一些,但缺點是相比暫存器,會慢一些,而且會產生數量更多的指令。     直譯器在執行時,會對載入的指令一條一條的執行,前面說過計算操作都是基於運算元棧進行的。 例: