執行引擎,一個逼格很高的名字,就是用來執行java字節碼的一段代碼,執行代碼的代碼讀起來很拗口。與物理機的執行引擎不一樣,物理機的執行引擎是創建在cpu 操做系統上的,JVM的執行引擎是須要本身編寫的。執行引擎執行java字節碼的方式有兩種,解釋執行和編譯執行,編譯執行就是把字節碼編譯成native code讓物理機去執行。不一樣的虛擬機採用的方式是不一樣的,虛擬機規範對次不作要求,只要求他們有一致的外觀,所謂一致的外觀指的是我同一個字節碼在張三的虛擬機和李四的虛擬機上的執行結果要是 一致的。 java
每個線程在JVM的內存區裏都有一段本身的線程私有的內存區域,這段區域就是虛擬機棧。線程從執行一個方法開始到一個方法執行完畢對應一個棧幀的入棧到出棧的過程,執行一個方法須要的全部內存、數據都在一個棧幀裏。this
具體來講,每個棧幀包括:局部變量表、操做數棧、動態連接方法返回的地址。操作系統
線程在執行一個方法的時候就要分配一個新的棧幀,棧幀裏的局部變量表和操做數棧的大小在class文件的方法表的Code屬性表裏記錄。線程
一個方法在執行的時候若是調用了另外一個方法,會有新的棧幀被壓入相應線程的虛擬機棧裏。字節碼指令是面向棧的,字節碼指令面向的棧是棧頂的棧幀裏的操做數棧。翻譯
局部變量表裏存放一個方法裏使用到的變量,由於是方法裏的變量因此被稱爲局部變量。竟然是一個表,訪問裏面的內容就能夠經過索引來實現。若是方法是一個非static的方法,那麼這段方法有一個隱含的參數this指向調用該方法的對象,這個this參數被放在了局部變量表的第0個索引。 code
字節碼指令是面向棧的,因此須要一個棧來支持字節碼指令的執行,就是這個操做數棧。操做數棧一開始是空的,隨着程序的運行,字節碼指令頻繁的load store操做數,並對操做數棧裏的數據執行操做。對象
執行一個方法會建立一個新的棧幀,棧幀裏須要一個指向一個具體的方法的引用,持有這個引用是爲了實現方法調用過程當中的動態連接。blog
方法執行完畢有兩種狀況,一種是正常執行,一種是異常。繼承
正常執行完畢一個方法,須要返回該方法的調用者,這裏就涉及到一個現場恢復的問題。一個方法執行完畢後,PC寄存器要修改以便指向其調用者,若是有返回值須要把返回值放到調用者的操做數棧裏。索引
容許虛擬機實現者本身增長的一些規範裏沒有的信息
java編譯的時候沒有鏈接的步驟,全部的引用都是符號引用,須要再加載的時候或者執行的時候翻譯成直接引用即具體的入口地址。
若是一個符號引用知足「編譯期可知,運行期不變」,那麼這個符號引用在編譯期間就能夠判斷出其直接引用的地址,那麼在class文件加載過程當中的解析階段會翻譯成直接引用。從這裏能夠推測,不少方法在編譯的時候是不知道其具體實現方法在哪裏,好比一個接口,一個能夠被重寫的方法,這些方法是不肯定的,是可變的。
「不變」的方法主要是static修飾的方法和private修飾的方法,前者是與類相關的,後者是不可重寫的。這兩種方法的特色是我代碼寫好了,編譯經過了,他們的地址就肯定了(虛擬地址),這種符號引用到直接引用的翻譯稱爲解析,在class文件加載的過程當中完成。
從底層來講,若是java代碼在被編譯成class的時候,一個方法調用是經過 invokestatic invokespecial來實現的,那麼在class文件加載的時候的解析過程就會被翻譯成直接引用,這類方法被稱爲非虛方法。除此以外有一個特例,final修飾的方法雖然使用invokevitural來調用,可是沒法被重寫,他也是一種非虛方法。
分派是隻在有多個能夠選擇的方法下,選擇一個更合適的方法去執行,固然這個選擇的過程是由JVM來完成的,包括重載和重寫,說白了分派過程實現了重載和重寫方法的選擇。分派和解析並非符號引用翻譯成直接引用的先後的兩個階段,並非如今class加載階段完成解析而後在執行階段完成分派,這是從兩個維度去描述鏈接的過程。最直接的例子就是static方法也存在重載,那麼即在class文件加載的時候完成對static方法的解析,在解析的過程當中發現了該方法存在重載,此時還要經過相應的策略完成分配即選擇一個最合適的重載的方法。
一、靜態分派
類Man繼承自Human,Human man = new Man() Human稱爲變量的靜態類型,Man稱爲動態類型。個人理解是一個引用被聲明出來的類型是靜態類型,這個引用能夠指向的類型不只僅是該引用被聲明時的類型,能夠是其子類,一個引用具體指向的對象的類型被稱爲動態類型。
靜態類型和動態類型在聲明完成後是能夠改變的。動態類型的改變很好理解,一個父類的引用能夠指向多個不一樣的其子類實現,指向不一樣的實現就改變了不一樣的類型。靜態類型的改變指的是隻改變引用的類型而不改變引用指向的對象的類型。
於是不管一個變量的靜態類型如何變化,在程序結束的時候必定是可以肯定其靜態類型的,即在編譯的時候就能肯定靜態類型。那動態類型呢?動態類型只有在程序執行的時候才能肯定一個變量的動態類型。這麼說非常抽象,若是把一個變量的動態類型理解爲一個引用指向的具體的對象,再直白一點就是一個符號引用的直接引用,結合以前的知識,即一個符號引用只有在運行的時候才能知道其直接引用。
方法的重載涉及到重名方法的選擇,若是出現了靜態類型相同而實際類型不一樣的狀況,根據變量的靜態類型來決定重載的版本,又已知靜態類型在編譯階段就能夠缺點,因此能夠得出方法的重載在編譯的階段由編譯器完成。這種根據靜態類型選擇方法具體版本的分派稱爲靜態分派。
二、動態分派
動態分派從底層是使用invokevirtuall來實現的,這麼說其實不許確,應該是某些沒法在class文件加載過程當中完成符號引用到直接引用翻譯的方法即虛方法,要使用invokevitural指令來完成,而invokevitural的執行特色賦予了虛方法動態分派的特色。
invokevotural指令執行的時候,先找操做數棧頂第一個元素指向對象的實例類型。怎麼保證在執行invokevirtual的時候操做數棧頂必定是一個引用,萬一是一個基本類型的變量咋辦?我以爲這個保證應該是由java編譯器來完成的。無論如何,invokevirtuall找到了操做數棧頂的引用指向的堆裏的對象的實際類型,記爲C,若是在C的class文件裏的方法表集合裏找到了一個與符號引用所須要的方法符合的方法,那麼就檢查權限,權限檢查經過就返回C對象的該方法的直接引用,這樣翻譯就完成了。若是在C對象的class文件裏沒有找到,那麼就從下到上去找其父類裏是否有符合要求的方法。
因此從這裏能夠看到invokevirtuall指令的操做數是在棧裏,那麼只有在一個方法被執行的時候棧才能被建立,也就是說只有方法在執行的時候invokevirtuall才能找到他的操做數,也只有找到操做數才能完成符號引用到直接引用的轉換,這就是運行時動態分派,這就是重寫的本質。
三、單分派與多分派
分派的時候根據一個宗量稱爲單分派,反之是多分派。
首先是靜態分派,根據靜態類型,既包括調用者的靜態類型也包括參數的靜態類型。因此在靜態分配的時候是多分派。
其次是動態分配,即執行invokevirtual的時候,在靜態分配完畢後已經決定好了方法前面,此時的主要分配的目的是選擇方法的接受者,即此時操做數棧頂的那個引用的實際類型。因此動態分配是單分派。