虛擬機器位元組碼執行引擎——方法呼叫
文章目錄
方法呼叫並不等同於方法的執行。方法呼叫的唯一任務就是確定呼叫的是哪一個方法。一切方法呼叫在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(