深刻理解java虛擬機(5)---字節碼執行引擎

字節碼是什麼東西?html

如下是百度的解釋:java

字節碼(Byte-code)是一種包含執行程序、由一序列 op 代碼/數據對組成的二進制文件。字節碼是一種中間碼,它比機器碼更抽象。jvm

它常常被看做是包含一個執行程序的二進制文件,更像一個對象模型。字節碼被這樣叫是由於一般每一個 opcode 是一字節長,ide

可是指令碼的長度是變化的。每一個指令有從 0 到 255(或十六進制的: 00 到FF)的一字節操做碼,被參數例如寄存器或內存地址跟隨。函數

 

說了這麼多,你可能仍是不明白究竟是什麼東西。好吧,簡單點,就是java編譯之後的那個東東,「.class」文件。工具

因此class文件就是字節碼文件,是由虛擬機執行的文件。也就是java語言和C & C++語言的區別就是,整個編譯執行過程多了一個虛擬post

機這一步。這個在「深刻理解java虛擬機(3)---類的結構」 一文中已經解釋,這是一個里程碑式的設計。上一節講了虛擬機是如何加載優化

一個class的,這一節就講解虛擬機是如何執行class文件的。this

 

java虛擬機規範,規定了虛擬機字節碼的執行概念模型。具體的虛擬機能夠有不一樣的實現。url

運行時棧幀結構

棧是每一個線程獨有的內存。

棧幀存儲了局部變量表,操做數棧,動態鏈接,和返回地址等。

每個方法的執行 對應的一個棧幀在虛擬機裏面從如棧到出棧的過程。

只有位於棧頂的棧幀纔有有效的,對應的方法稱爲當前方法。

執行引擎運行的全部指令只針對當前棧幀和當前方法。

1.局部變量表

局部變量表存放的一組變量的存儲空間。存放方法參數和方法內部定義的局部變量表。

在java編譯成class的時候,已經肯定了局部變量表所需分配的最大容量。

局部變量表的最小單位是一個Slot。

虛擬機規範沒有明確規定一個Slot佔多少大小。只是規定,它能夠放下boolean,byte,...reference &return address.

reference 是指一個對象實例的引用。關於reference的大小,目前沒有明確的指定大小。可是咱們能夠理解爲它就是相似C++中的指針。

局部變量表的讀取方式是索引,從0開始。因此局部變量表能夠簡單理解爲就是一個表.

局部變量表的分配順序以下:

this 引用。能夠認爲是隱式參數。

方法的參數表。

根據局部變量順序,分配Solt。

一個變量一個solt,64爲的佔2個solt。java中明確64位的是long & double

爲了儘量的節約局部變量表,Solt能夠重用。

注意:局部變量只給予分配的內存,沒有class對象的準備階段,因此局部變量在使用前,必須先賦值。

2.操做數棧

操做數棧在概念上很像寄存器。

java虛擬機沒法使用寄存器,因此就有操做數棧來存放數據。

虛擬機把操做數棧做爲它的工做區——大多數指令都要從這裏彈出數據,執行運算,而後把結果壓回操做數棧。

好比,iadd指令就要從操做數棧中彈出兩個整數,執行加法運算,其結果又壓回到操做數棧中,看看下面的示例,

它演示了虛擬機是如何把兩個int類型的局部變量相加,再把結果保存到第三個局部變量的:

begin

iload_0 // push the int in local variable 0 onto the stack

iload_1 // push the int in local variable 1 onto the stack

iadd // pop two ints, add them, push result

istore_2 // pop int, store into local variable 2

end

操做數棧 的數據讀取、寫入就是出棧和如棧操做。

3.動態鏈接

每一個棧幀都包含一個指向運行時常量池的引用,持有這個引用是爲了支持動態鏈接。

符號池的引用,有一部分是在第一次使用或者初始化的時候就肯定下來,這個稱爲靜態引用。

還有一部分是在每次執行的時候採起肯定,這個就是動態鏈接。

4.方法返回地址

方法只有2中退出方式,正常狀況下,遇到return指令退出。還有就是異常退出。

正常狀況:通常狀況下,棧幀會保存 在程序計數器中的調用者的地址。虛擬機經過這個方式,執行方法調用者的地址,

而後把返回值壓入調用者中的操做數棧。

異常狀況:方法不會返回任何值,返回地址有異常表來肯定,棧幀通常不存儲信息。

5.方法調用

方法調用階段不是執行該方法,而僅僅時確認要調用那個方法。class文件在編譯階段沒有鏈接這一過程,、

因此動態鏈接這個在C++就已經有的技術,在java運用到了一個新的高度。全部的函數(除了私有方法,構造方法 & 靜態方法,下同),理論上

均可以時C++裏面的虛函數。因此全部的函數都須要經過動態綁定來肯定「明確」的函數實體。

解析

全部方法調用的目標方法都是常量池中的符號引用。在類的加載解析階段,會將一部分目標方法轉化爲直接引用。(能夠理解爲具體方法的直接地址)

能夠轉化的方法,主要爲靜態方法 & 私有方法。

Java虛擬機提供5中方法調用命令:

invokestatic:調用靜態方法

invokespecial:調用構造器,私有方法和父類方法

invokevirtual:調用虛方法

invokeinterface:調用接口方法

invokedynamic:如今運行時動態解析出該方法,而後執行。

invokestatic & invokespecial 對應的方法,都是在加載解析後,能夠直接肯定的。因此這些方法爲非虛方法。

java規定 final修飾的是一種非虛方法。

分派

靜態分派

先看一個例子:

package com.joyfulmath.jvmexample.dispatch;

import com.joyfulmath.jvmexample.TraceLog;

/**
 * @author deman.lu
 * @version on 2016-05-19 13:53
 */
public class StaticDispatch {
    static abstract class Human{

    }

    static class Man extends Human{

    }

    static class Woman extends Human{

    }

    public void sayHello(Human guy)
    {
        TraceLog.i("Hello guy!");
    }

    public void sayHello(Man man)
    {
        TraceLog.i("Hello gentleman!");
    }

    public void sayHello(Woman man)
    {
        TraceLog.i("Hello lady!");
    }

    public static void action()
    {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch dispatch = new StaticDispatch();
        dispatch.sayHello(man);
        dispatch.sayHello(woman);
    }
}
05-19 13:58:05.538 14881-14881/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)]
05-19 13:58:05.539 14881-14881/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)]

結果執行了public void sayHello(Human guy)函數。這不是應該多態嗎?

Human man = new Man();

這裏的Human咱們理解爲靜態類型,後面的Man是實際類型。咱們在編譯器只知道靜態類型,後面的實際類型等到動態鏈接的時候才知道。

因此對於sayHello方法,虛擬機在重載時,是經過參數的靜態類型,而不是實際類型來判斷使用那個方法的。

若是對類型作強制轉換:

    public static void action()
    {
        Human man = new Man();
        Human woman = new Woman();
        StaticDispatch dispatch = new StaticDispatch();
        dispatch.sayHello(man);
        dispatch.sayHello(woman);
        dispatch.sayHello((Man)man);
        dispatch.sayHello((Woman)woman);
    }
05-19 14:08:29.000 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)]
05-19 14:08:29.001 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello guy! [at (StaticDispatch.java:24)]
05-19 14:08:29.001 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello gentleman! [at (StaticDispatch.java:29)]
05-19 14:08:29.002 21838-21838/com.joyfulmath.jvmexample I/StaticDispatch: sayHello: Hello lady! [at (StaticDispatch.java:34)]

若是強轉了之後,類型也跟着變化了。

靜態分配的典型應用是方法重載。可是方法重載有時候不是惟一的,因此只能選合適的。

好比:

    public void sayHello(int data)
    {
        TraceLog.i("Hello int!");
    }

    public void sayHello(long  data)
    {
        TraceLog.i("Hello long");
    }

當sayHello(1)的時候,通常狀況下會調用int型的方法,可是若是註釋調,只有long型的方法,long型參數方法就會被調用。

 

動態分派

上面講的是重載,這裏是重寫(@Override)

package com.joyfulmath.jvmexample.dispatch;

import com.joyfulmath.jvmexample.TraceLog;

/**
 * @author deman.lu
 * @version on 2016-05-19 14:26
 */
public class DynamicDispatch {
    static abstract class Human{
        protected abstract void sayHello();
    }

    static class Man extends Human{

        @Override
        protected void sayHello() {
            TraceLog.i("Hello gentleman!");
        }
    }

    static class Woman extends Human{

        @Override
        protected void sayHello() {
            TraceLog.i("Hello lady!");
        }
    }

    public static void action()
    {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
        man = new Woman();
        man.sayHello();
    }
}

先來看上面標紅的這句:方法要解析man 的sayhello,問題是man是什麼東西,我在解析的時候,是不知道的。因此「man.sayHello();」具體執行的那個類的方法,是須要在虛擬機

動態鏈接的時候才知道,這個就是多態。若是使用javap分析就能夠知道這句話,在class文件裏面是ynamicDispatch$Human: sayHello. 是的class文件不知道這個sayhello到底要去

調哪一個方法。

invokevirtual指令解析的過程大概以下:首先在操做數棧裏第一個元素的實際類型,即爲C。

若是在類型C中找到與常量描述符相同的類名和方法,則權限校驗經過後,即爲找到該法方法,則返回這個方法的直接引用。

不然,對C的父類進行依次查找。

這個過程通俗一點就是,先從當前類裏面尋找「同名」的該方法,若是沒有,就從C的父類裏面找,知道找到爲止!

這個找到的方法,就是咱們實際要調的方法。

若是找不到,就是exception。通常狀況下,編譯工具會幫咱們避免這種狀況。

單分派和多分派

概念上理解比較麻煩,說白了一點就是重載和重寫都存在的狀況:

package com.joyfulmath.jvmexample.dispatch;

import com.joyfulmath.jvmexample.TraceLog;

/**
 * @author deman.lu
 * @version on 2016-05-19 15:02
 */
public class MultiDispatch {
    static class QQ{}
    static class _360{}

    public static class Father{
        public void hardChoice(QQ qq){
            TraceLog.i("Father QQ");
        }

        public void hardChoice(_360 aa){
            TraceLog.i("Father 360");
        }
    }

    public static class Son extends Father{
        public void hardChoice(QQ qq){
            TraceLog.i("Son QQ");
        }

        public void hardChoice(_360 aa){
            TraceLog.i("Son 360");
        }
    }

    public static void action()
    {
        Father father = new Father();
        Father son = new Son();
        father.hardChoice(new _360());
        son.hardChoice(new QQ());
    }
}
05-19 15:07:44.429 29011-29011/com.joyfulmath.jvmexample I/MultiDispatch$Father: hardChoice: Father 360 [at (MultiDispatch.java:19)]
05-19 15:07:44.429 29011-29011/com.joyfulmath.jvmexample I/MultiDispatch$Son: hardChoice: Son QQ [at (MultiDispatch.java:25)]

結果沒有任何懸念,可是過程仍是須要明確的。hardChoice的選擇是在靜態編譯的時候就確認的。

而son.hardchoise 已經確認了函數的類型,只是須要進一步確認實體類型。因此動態鏈接是單分派。

 

動態語言支持:

使用C++語言能夠定義一個調用方法:

void sort(int list[],const int size,int (*compare)(int,int));

可是java很難作到這一點,

void sort(List list,Compare c);Compare 通常要用接口實現。

在java 1.7 有一種方法能夠支持該功能 MethodHandle。

這部份內容,因爲我本地環境沒法配置還調用,將會再後續更新。

 

鋪墊了這麼多,下面來說講字節碼的執行

6.基於棧的字節碼執行引擎

基於棧的指令集 和基於寄存器的指令集。

先看一個加法過程:

iconst_1

iconst_1

iadd

istore_0

這是基於棧的,也就是上文說的操做數棧。

先把2個元素要入棧,而後相加,放回棧頂,而後把棧頂的值存在slot 0裏面。

基於寄存器的就不解釋了。

基於寄存器 和基於棧的指令集如今都存在。因此很難說孰優孰劣。

基於棧的指令集 是和硬件無關的,而基於寄存器則依賴於硬件基礎。基於寄存器在效率上優點。

可是虛擬機的出現,就是爲了提供跨平臺的支持,因此jvm的執行引擎是基於棧的指令集。

    public int calc()
    {
        int a = 100;
        int b = 200;
        int c = 300;
        return (a+b)*c;
    }

如下是javap的分析結果:

 

如下圖片描述了整個執行過程當中代碼,操做數棧,& 局部變量表的變化。

 

 

這些過程只是一個概念模型,實際虛擬機會有不少優化的狀況。

聲明:本文相關圖片來之參考書面,相關版權歸原做者全部。

參考:

《深刻理解java虛擬機》 周志明

相關文章
相關標籤/搜索