老馬的JVM筆記(五)----虛擬機字節碼的執行

5.1 運行時棧幀結構

棧幀(Stack Frame)用於存儲方法的信息。方法有關的才說棧。幀就是一塊裝棧的幀。棧幀用於存儲方法的局部變量棧,操作數棧,動態連接,方法返回地址等。虛擬機棧裏就都是這種棧幀,棧中有幀,幀中有棧。線程中充滿了方法棧,方法調用方法,一個線程就會有一連串的棧幀。

1.局部變量表:方法中存有很多變量。在Java文件編譯成Class文件時,虛擬機就爲該方法需要的局部變量表的最大容量進行了確定(代碼是靜態的,運行是動態的,最大容量一開始就定下來,爆了怎麼辦?)。這個容量用很久以前說過的slot來規定。一個slot可以存放一個標準類型的變量。32位變量1slot,64位變量2slot,連續且不可分割。

局部變量表第0個變量是this,當然了,不能使static的方法,因爲static沒有this。第一個開始纔是正經變量。

* 不使用的大對象應該手動處理:

public void hello(){
    {
        byte[] holder = new byte[64*1024*1024];
    }
    System.gc();
}
public void hello(){
    {
        byte[] holder = new byte[64*1024*1024];
    }
    int i = 0;
    // could be: holder = null;
    System.gc();
}

第一段的gc無效,第二段雖然加了沒啥用的一句話,但就gc了。第一段代碼框內部分執行完,雖然走出了holder的作用範圍,但沒有對局部變量表作任何事情,所以holder佔的slot還在。所以gc不能進行。隨便加一句話,走出範圍並改動了局部變量表,slot可以被清除重用。所以大對象不用了,一定要處理掉啊,不能攢着。但是直接設爲null確實有點暴力了。

類變量的準備階段會給每個變量賦個初值,但局部變量沒有這個方便,所以一定要搞出來一個初始值。

2.操作數棧:

和彙編課學的差不多,後進先出。操作就是方法中賦值啊,計算啊之類的。棧會自己找出一些類型問題,操作error。

3.動態連接:

用於指向運行時常量池中自己屬於的方法。

4.方法返回地址:

方法只有兩種情況會退出,第一是返回指令,return;第二種是異常。棧幀中要存有這兩種方法各自返回的地址。返回結束,當前棧幀就算推出方法棧了。

5.2 方法調用

找方法。由於多態,方法名對不代表方法對。方法調用就是找我到底調用那個方法。Class文件中的常量池中存放的並不是方法,而是方法的符號引用。在類的解析階段會解析出來一點很明確的方法(eg:靜態方法和私有方法)。其餘的都要動態找。

1.解析:

類解析階段可以解析出來的非常明確的方法。靜態方法,私有方法,實例構造器,父類方法。這都是實打實的老實方法。

2.分派(dispatch):

分派針對虛擬機解決Java的多態問題。

class A{
    static class Human{}

    static class Man extends Human{}

    static class Women extends Human{}

    public static void main(String[] args){
        Human man = new Man();
        Human women = new Women();
    }
}

Human man = new Man(); 在這句話裏,Human叫變量的靜態類型(static type),也叫外觀類型(Apparent Type)。Man纔是變量的實際類型(Actual Type)。靜態類型編譯時可知,實際類型只有運行時可知。可以理解,靜態類型你只會定義一次,就是在聲明對象時。但實際類型隨時可以改,只要家譜裏有靜態類型就可以。

// 靜態類型變化
sayHello((Man) man);
// 實際類型變化
man = new Woman();

到底調用誰,取決於虛擬機中調用方的調用方式。如果吃靜態類型,就只看靜態類型的方法,如果吃動態類型變量,就只看動態類型的方法。這個調用過程也是自下而上的。 

方法的接收者和方法的參數統稱爲方法的宗量(argument)。在編譯階段,方法解析要考慮調用者的靜態類型,以及方法參數,所以說靜態爲多分派。在運行階段,方法參數已定,只有方法的調用者會影響方法的選擇,所以運行時動態單分配。 

public Father father = new Son("first"); // 既要看Father型,又要看方法中的參數

father.getSon(); //調用時看調用者

5.4 動態類型

動態類型語言:在運行期才確定類型,例如javascript, 萬物都是var。Java,C++都是在編譯時就要確定變量方法的類型,屬於靜態類型語言。實現原理爲變量本身無類型,但變量的值有類型。

例如var str = "abcdefg":單看str,無法確定類型,但後面的值看似String,那就將他劃爲String類型值。

靜態類型編譯器可以發現錯誤,適合大規模程序設計;動態類型簡單易寫,適用於高效率開發。

JVM爲了支持動態類型語言,設計了一些機制。java.lang.invoke。爲了實現類似函數指針的功能,java提供了MethodHandle。

static class ClassA{
    public void println(String s){
        // simulate System.out
        System.out.println(s)
    }
}

public static void main(String[] args) throws Throwable{
    Object obj = System.currentTimeMills() % 2 == 0 ? System.out : new ClassA();
    // randomly pick one class

    getPrintlnMH(obj).invokeExact("icyfenix");
}

// notice the return type, it's "MethodHandle"
private static MethodHandle getPrintlnMH(Object receiver) throws Throwable{
    MethodType mt = MethodType.methodeType(void.class, String.class);
    // first param is return type, second is concrete input param
    
    // lookup()->MethodHandles.lookup, looks up some method qualified
    return lookup().findVirtual(receiver.getClass(), "println", mt).bindTo(receiver);
    // return a concrete method, rather in System.out or in ClassA
    // depends on type of receiver
    // after all, each type contains a "println"

    // All in all, this example shows that a function can be decided dynamically
}

與反射機制(reflection)相比,反射實在模擬Java代碼層,而MethodHandle在模擬字節碼層。(反射日後細談)

實際例子:

class Test{
    class GrandFather{
        void thinking(){
            System.out.println("I'm grandfather.");
        }
    }

    class Father extends GrandFather{
        void thinking(){
            System.out.println("I'm father.");
        }
    }

    class Son extends Father{
        void thinking(){
            try{
                // here, we want to invoke gf's thinking
                MethodType mt = MethodType.methodType(void.class);
                MethodHandle mh = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
                mh.invoke(this);
                // invoke "grandfather's this"
            } catch(Throwable e){}
        }
    }

    public static void main(String[] args){
        (new Test.new Son()).thinking();
        // I'm grandfather.
    }
}

5.5 字節碼解釋執行引擎

由於筆者對JVM接觸尚少,對本章節的實際應用不甚瞭解,所以代碼和很多具體原理都是淺嘗輒止,只求看懂,暫時不求應用。

解釋執行:通過解釋器執行;編譯執行:通過即時編譯器,產生本地代碼,再執行。

在物理機執行代碼的過程中,要經歷詞法分析,語法分析,生成抽象語法樹。這是編譯過程。C/C++將這些過程獨立於執行引擎,Java把抽象樹之前步驟獨立,算是半獨立引擎。Javac編譯器完成詞法分析,語法分析,抽象語法樹,再生成線性字節碼指令流,而解釋器在JVM中,所以是半獨立。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章