1. 程式人生 > >虛擬機器位元組碼執行引擎——方法呼叫

虛擬機器位元組碼執行引擎——方法呼叫

文章目錄


方法呼叫並不等同於方法的執行。方法呼叫的唯一任務就是確定呼叫的是哪一個方法。一切方法呼叫在class檔案裡面存的都是符號引用,而不是方法在記憶體中的入口地址。

一、解析

解析階段幹什麼:

解析階段是解析在程式執行之前就確定的東西。

解析階段解析的東西:

私用方法,靜態方法這些非虛方法

解析階段使用到的命令:

  • invokespecial:呼叫私有方法、父類方法、例項構造器<init>
  • invokestatic:呼叫靜態方法。但不包括final修飾的靜態方法(用的是invokespecial)

呼叫位元組碼的指令

呼叫位元組碼的指令除了上面那兩個,還有

  • invokevirtual :呼叫所有的虛方法 與final修飾的非靜態方法(final修飾的非靜態方法不是虛方法)。
  • invokeinterface: 呼叫介面方法。會在執行時確定一個實現此介面的物件
  • invokedynamic:先在執行時動態解析限定符所引用的方法,然後再執行該方法。前面4條指令都是固化在虛擬機器內部的,而這條指令的分派邏輯是由使用者所設定的載入程式決定的(這裡不太明白)。lambda表示式就用到了這條指令。

這裡是使用這5個指令的例子:

public class Main {
    public static void sayHello() {
        System.out.println("Hello World!");
    }

    public
static final void sayHello2() { System.out.println("Hi,world!"); } public final void sayHello3() { System.out.println("HaHa!!"); } public static void dynamicMethod(IA ia){ ia.interfaceA(); } public static void main(String[] args) { sayHello();//這裡使用的是invokestatic命令 sayHello2();//這裡使用的是invokestatic命令 Main m = new Main();//呼叫<init>()是使用invokespecial m.sayHello3();//使用的是invokevirtual命令,但是sayHello3是個非虛方法,因為它用final修飾的 IA ia = new IA() { @Override public void interfaceA() { System.out.println("我是介面方法"); } }; ia.interfaceA();//這裡使用的是invokeinterface命令 AbstractClass ac=new AbstractClass() { @Override public void abstractMethod() { System.out.println("我是虛方法"); } }; ac.abstractMethod();//使用的是invokevirtual指令 dynamicMethod(()->{ //使用lambda表示式時使用的是invokedynamic指令,呼叫dynamicMethod任然使用的是invokestatic指令 System.out.println("lambda表示式子"); }); IA temp=new IA(){//呼叫IA的例項構造器使用的是invokespecial指令 @Override public void interfaceA() { System.out.println("沒有使用lambda表示式"); } }; dynamicMethod(temp);//呼叫dynamicMethod任然使用的是invokestatic指令 } } interface IA { void interfaceA(); } abstract class AbstractClass { public abstract void abstractMethod(); } class SubClassA extends AbstractClass{ @Override public void abstractMethod() { System.out.println("SubClassA重寫了父類方法"); } } class SubClassB extends AbstractClass { @Override public void abstractMethod() { System.out.println("SubClassB重寫了父類方法"); } }

執行結果:
在這裡插入圖片描述

然後javap -verbose Main,得到結果如下(我只截了main方法的):

在這裡插入圖片描述

虛方法、非虛方法:

  • 非虛方法:包括靜態方法、私有方法。《java虛擬機器規範》明確規定了final修飾的方法也為非虛方法。
  • 虛方法:不是非虛方法的方法。

二、分派

按分派呼叫的方式可以分為靜態和動態方式。按分派的宗量可分為單分派和多分派。

2.1 靜態分派

首先來一個例子,看一下輸出:

public class Main {
    public void sayHello(AbstractClass ac) {
        System.out.println("Hello World!");
    }

    public void sayHello(SubClassA a) {
        System.out.println("SubClassA say hello");
    }

    public void sayHello(SubClassB b) {
        System.out.println("SubClassB say hello");
    }
    public static void main(String[] args) {
        Main m = new Main();
        AbstractClass ac1 = new SubClassA();//靜態型別是AbstractClass,實際型別是SubClassA
        AbstractClass ac2 = new SubClassB();//靜態型別是AbstractClass,實際型別是SubClassB
        m.sayHello(ac1);//呼叫sayyHello方法時使用的是invokevirtual指令
        m.sayHello(ac2);
    }
}

abstract class AbstractClass {
    public abstract void abstractMethod();
}

class SubClassA extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("SubClassA重寫了父類方法");
    }
}

class SubClassB extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("SubClassB重寫了父類方法");
    }
}

輸出結果是:
在這裡插入圖片描述

靜態型別和實際型別:

AbstractClass ac1 = new SubClassA();//靜態型別是AbstractClass,實際型別是SubClassA

靜態型別和實際型別的區別:
首先我們看靜態型別變化和實際型別變化

AbstractClass ac=new SubClassA();
ac=new SubClassB();//實際型別變化

m.sayHello((SubClassA) ac);//靜態型別變化,ac本身的靜態型別是沒有變化的
m.sayHello((SubClassB) ac);//靜態型別變化,ac本身的靜態型別是沒有變化的
  • 靜態型別是在編譯期就確定的,而實際型別是在執行期確定的。

過載是靜態分派的典型應用。過載往往是找出最合適的匹配方法。過載方法的匹配順序。
byte–>short–>int–>long–>float–>double–>對應的裝箱類–>(現在可以上轉型了)

例子:

import java.io.Serializable;
public class Main {
    public void doSomething(char c) {
        System.out.println("char c");
    }

    public void doSomething(int c) {
        System.out.println("int c");
    }

    public void doSomething(long c) {
        System.out.println("long c");
    }


    public void doSomething(float c) {
        System.out.println("float c");
    }


    public void doSomething(double c) {
        System.out.println("doulbe c");
    }


    public void doSomething(Double c) {
        System.out.println("Double c");
    }


    public void doSomething(Object c) {
        System.out.println("Object c");
    }


    public void doSomething(Character c) {
        System.out.println("Character c");
    }


    public void doSomething(Serializable c) {
        System.out.println("Serializable c");
    }


    public static void main(String[] args) {
        Main m = new Main();
        char c = 3;
        m.doSomething(c);

    }
}

2.2 動態分派

用一個例子來解釋動態分派:

public class Main {
    public static void main(String[] args) {
        Main m = new Main();
        AbstractClass a=new SubClassA();
        AbstractClass b=new SubClassB();
        a.abstractMethod();//解析後的指令都為Method AbstractClass.abstractMethod:()V
        b.abstractMethod();//解析後的指令都為Method AbstractClass.abstractMethod:()V

    }
}

class AbstractClass {
    public void abstractMethod() {
    }
}

class SubClassA extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("SubClassA重寫了父類方法");
    }
}

class SubClassB extends AbstractClass {
    @Override
    public void abstractMethod() {
        System.out.println("SubClassB重寫了父類方法");
    }
}

javap -verbose Main後的結果(注意25行和29行):
在這裡插入圖片描述
執行結果:
在這裡插入圖片描述
問題來了,既然解析是的指令都一樣,為什麼執行後的結果不一樣呢??在說明為什麼之前,我們先知道個概念:呼叫方法的物件稱為接受者

invokevirtual執行時的解析過程

  • 找到運算元棧頂的第一個元素的實際型別,記做C。
  • 如果在型別C中找到與常量中的描述符和簡單名詞相符的方法,則進行訪問許可權校驗,如果通過就返回這個方法的直接引用,查詢結束;如果不通過,則返回java.lang.IllegalAccessError。
  • 否則,按照繼承關係從下往上依次進行第2個步奏。
  • 如果始終沒找到合適的 方法,則丟擲java.lang.AbstractMethodError。

2.3 單分派與多分派

方法的接受者與方法的引數統稱為方法的宗量。

public class Main {
    public static void main(String[] args) {
        Father father=new Father();
        Father son=new Son();
        father.hardChoice(new _360());//Method Father.hardChoice:(L_360;)V
        son.hardChoice(new QQ());// Method Father.hardChoice:(LQQ;)V
    }
}

class QQ {

}

class _360 {
}

class Father{
    public void hardChoice(QQ arg){
        System.out.println("father choose qq");
    }
    public void hardChoice(_360 arg){
        System.out.println("father choose 360");
    }
}

class Son extends Father{
    public void hardChoice(QQ arg){
        System.out.println("son choose qq");
    }
    public void hardChoice(_360 arg){
        System.out.println("son choose 360");
    }
}

javap -verbose Main之後(注意24和35):
在這裡插入圖片描述

靜態解析時依據的是靜態型別引數,執行時是依據接受者的實際型別.

目前為止java語言是一門靜態多分派動態單分派的語言。

2.4 虛擬機器動態分派的實現

每個虛擬機器對怎樣實現動態分派是有所區別的。動態分派是一個頻繁的動作。基於效能的考慮,最常用的“穩定手段”是在類的方法區中建立虛方法表(與之對應在invokeinterface時也會用到介面方法表)。

比如上面程式碼的虛方法表結構如下:
在這裡插入圖片描述

虛方法表存的是各個方法的實際入口地址。如果某個方法在子類中沒有被重寫,那麼父類和子類虛方法表中該方法的入口地址一樣

還要注意的是具有相同簽名的方法,在子類和父類的虛方法表中都應當具有一樣的索引序號

三、動態語言支援

3.1 動態語言型別

什麼是動態語言型別

型別檢查的主體在程式執行期間,而不是編譯期間。滿足這個特徵的語言有JavaScript、PHP、Clojure、Groovy、Jython、Python、Ruby。與之相反的就是靜態型別語言,如Java、C++。

ECMAScripte動態型別語言與java等靜態型別語言的區別

一句話:“變數無型別而變數值才有型別”

動態型別語言和靜態型別語言各自的優缺點

靜態型別語言最顯著的好處就是提供了嚴謹的型別檢查,這樣與型別相關的問題在編碼期間就能及時發現,利於穩定性及程式碼達到更大規模。動態型別語言在執行期間確定型別,這給開發人員提供了更大的靈活性,在某些靜態語言需要大量程式碼來實現的功能,由動態語言編寫會更加清晰和簡潔,意味著開發效率的提升。

3.2 MethodHandle

MethodHandle是一種動態確定方法的機制。和C++中的方法指標類似。

例子:

import static java.lang.invoke.MethodHandles.lookup;//這和其他的import有什麼區別

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;

public class Main {
    static class ClassA {
        public void println(String s) {
            System.out.println(s);
        }
    }

    public static void main(String[] args) throws Throwable {
        Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
        //呼叫方法
        getPrintlnMH(obj).invokeExact("fengli");
    }

    /**
     * @param reveiver 方法的接受者
     * @return
     */
    private static MethodHandle getPrintlnMH(Object reveiver) throws Throwable {
        //方法的返回型別為void,方法的引數為一個String型別的引數
        MethodType mt = MethodType.methodType(void.class, String.class);
        //在reveiver型別中查詢叫做println的mt型別的方法。
        return lookup().findVirtual(reveiver.getClass(), "println", mt).bindTo(reveiver);
    }
}


上面的功能也可以用反射完成。那麼MethodHandle和反射的區別:

  • 最大的區別是MethodHandle的設計是為了服務於所有java虛擬機器之上的語言,包括Java語言。而反射值服務於java語言
  • 反射和方法控制代碼都是模擬方法呼叫,反射是在java程式碼層次的模擬,而MethodHandle是在位元組碼層次的模擬。MethodHandles.lookup中的三個方法findStatic()、findVirtual()、findSpecial分別對應於位元組碼層面的invokestatic、invokevirtual、invokespecial。
  • Reflection中的java.lang.reflect.Method物件包含的資訊遠比MethodHandle機制中的java.lang.invoke.MethodHandle多。也就是反射是重量級的 ,而MethodHandle是輕量級的。

3.3 invokedynamic指令

invokedynamic和方法控制代碼一樣都是為了解決——如何把查詢目標方法的決定權交給使用者問題。MethodHandle和invokedynamic指令的區別:MethodHandle是用上層java程式碼和API實現的,而invokedynamci是用位元組碼和Class中其他屬性、常量來實現的

invokedynamic指令的介紹

含有invokedynamic的地方稱為動態呼叫點,這個指令的第一個引數是CONSTANT_InvokeDynamic_info常量(包含引導方法、方法型別、名稱)。

例子:

import static java.lang.invoke.MethodHandles.lookup;//這和其他的import有什麼區別

import java.lang.invoke.*;

public class Main {


    public static void main(String[] args) throws Throwable {
      INDY_BootstrapMethod().invokeExact("fengli");
    }

    public static void testMethod(String s){
        System.out.println("hello String:"+s);
    }

    /**
     * 引導方法
     * @param lookup
     * @param name
     * @param mt
     * @return 表示真正要執行的目標方法呼叫
     * @throws Throwable
     */
    public static CallSite BootstrapMethod(MethodHandles.Lookup lookup,String name, MethodType mt) throws Throwable{
        return new ConstantCallSite(lookup.findStatic(Main.class,name,mt));
    }

    private static MethodType MT_BootstrapMethod(){
        return MethodType.fromMethodDescriptorString("(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;",null);
    }

    private static  MethodHandle MH_BootstrapMethod() throws Throwable{
        return lookup().findStatic(Main.class,"BootstrapMethod",MT_BootstrapMethod());
    }

    private static  MethodHandle INDY_BootstrapMethod() throws Throwable{
        CallSite cs=(CallSite) MH_BootstrapMethod().invokeWithArguments(lookup(),"testMethod",MethodType.fromMethodDescriptorString("(Ljava/lang/String;)V",null));
        return cs.dynamicInvoker();
    }
}


首先,我們要知道僅依靠java語言的編譯器javac是沒有辦法生成帶有invokedynamic指令的位元組碼的(lambda表示式例外)。我們需要使用[INDY]將位元組碼轉換為我們最終所要的位元組碼。

3.4 方法分派的例子

invokedynamic與其他4條invoke指令最大的差別就是它的分派邏輯並不是虛擬機器決定的,而是有程式設計師決定的。
例子,獲取族類的方法:

import static java.lang.invoke.MethodHandles.lookup;//這和其他的import有什麼區別

import java.lang.invoke.*;
import java.lang.reflect.Field;

public class Main {
    class GrandFather {
       public void thinking() {
            System.out.println("i am grandfather");
        }
    }

    class Father extends GrandFather {
       public  void thinking() {
            System.out.println("i am father");
        }
    }

    class Son extends Father {
       public void thinking() {
            try {
                //jdk1.7
               /* MethodType mt = MethodType.methodType(void.class);
                MethodHandle mh = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
                mh.invoke(this);*/
                //jdk1.8
                MethodType mt = MethodType.methodType(