05、JVM实战 - 字节码和类的加载(一) -- 方法调用

此文章对应书中8.3节内容。

方法调用并不等同于方法中的代码被执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还未涉及方法内部的具体运行过程。

1. 方法绑定机制

在JVM中,将符号引用转换为调用方法的直接引用与方法的绑定机制相关。

1.1 静态链接:

当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。这类方法的调用也被称之为解析。

1.2 动态链接:

如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也称之为动态链接。

这2种链接方法对应的方法的绑定机制为:早期绑定和晚期绑定。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。

1.3 早期绑定:(解析)

早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。

1.4 晚期绑定:

如果被调用的方法在编译期无法被确定下来,只能在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

代码解释:

public class AnimalTest {

    public void showAnimal(Animal animal) {

        animal.eat(); // 表现为晚期绑定,并不知道到底调用的是谁的eat
    }

    public void showHunt(Huntable h) {

        h.hunt(); // 表现为晚期绑定,不知道调用的谁的hunt
    }
}

interface Huntable {

    void hunt();
}

class Animal {

    public void eat() {

        System.out.println("动物进食");
    }
}

class Cat extends Animal implements Huntable {

    // 后加的,所以下面截图中没有
    public Cat() {

        super(); //表现为:早期绑定
    }

    public Cat(String name) {

        this(); //表现为:早期绑定
    }

    @Override
    public void eat() {

        super.eat(); //表现为:早期绑定
        System.out.println("猫吃鱼");
    }

    @Override
    public void hunt() {

        System.out.println("猫捕耗子,天经地义");
    }
}

class Dog extends Animal implements Huntable {

    @Override
    public void eat() {

        System.out.println("狗吃骨头");
    }

    @Override
    public void hunt() {

        System.out.println("狗拿耗子,多管闲事");
    }
}

jclasslib查看:

1、 Animal类的初始化方法:invokespecial早期绑定,知道是谁调用的;
 
2、 AnimalTest类型的初始化方法:invokespecial早期绑定;
 
3、 showAnimal方法:invokevirtual为晚期绑定,不知道是什么动物;
 
4、 showHunt方法:invokeinterface为晚期绑定,不知道是什么动物捕食;
 

随着高级语言的横空出世,类似于Java一样的基于面向对象的编程语言如今越来越多,尽管这类编程语言在语法风格上存在一定的差别,但是它们彼此之间始终保持着一个共性,那就是都支持封装、继承和多态等面向对象特性,既然这一类的编程语言具备多态特性,那么自然也就具备早期绑定和晚期绑定两种绑定方式。

Java中任何一个普通的方法其实都具备虚函数的特征,它们相当于C++语言中的虚函数(C++中则需要使用关键字virtual来显式定义)。如果在Java程序中不希望某个方法拥有虚函数的特征时,则可以使用关键字final来标记这个方法。

2. 非虚方法和虚方法:

  • 如果方法在编译期就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法

  • 静态方法,私有方法,final方法,实例构造器(是重载,构造器是不能被重写的),父类方法(在子类重写方法中通过super点调用的父类方法)都是非虚方法

  • 其他方法称为虚方法

2.1 调用指令

虚拟机中提供了以下几条方法调用指令:

  • 普通调用指令

  • invokestatic:调用静态方法,解析阶段确定唯一方法版本

  • invokespecial:调用< init >方法、私有及父类方法,解析阶段确定唯一方法版本

  • invokevirtual:调用所有虚方法(final修饰的除外)

  • invokeinterface:调用接口方法

  • 动态调用指令

  • invokedynamic:动态解析除需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。其中invokestatic指令和invokespercial指令调用的方法称为非虚方法,其余的(final修饰的除外)称为虚方法。

/**
 * 解析调用中非虚方法、虚方法的测试
 *
 * invokestatic指令和invokespecial指令调用的方法称为非虚方法
 */
class Father {

    public Father() {

        System.out.println("father的构造器");
    }

    public static void showStatic(String str) {

        System.out.println("father " + str);
    }

    public final void showFinal() {

        System.out.println("father show final");
    }

    public void showCommon() {

        System.out.println("father 普通方法");
    }
}

public class Son extends Father {

    public Son() {

        //invokespecial 非虚方法
        super();
    }

    public Son(int age) {

        //invokespecial 非虚方法
        this();
    }

    //不是重写的父类的静态方法,因为静态方法不能被重写!
    public static void showStatic(String str) {

        System.out.println("son " + str);
    }

    private void showPrivate(String str) {

        System.out.println("son private" + str);
    }

    public void show() {

        //invokestatic 非虚方法
        showStatic("baidu.com");

        //invokestatic 非虚方法
        super.showStatic("good!");

        //invokespecial 非虚方法
        showPrivate("hello!");

        //invokevirtual
        //虽然字节码指令中显示为invokevirtual,但因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。
        showFinal();

        //invokespecial 非虚方法
        super.showCommon();

        //invokevirtual 虚方法
        //有可能子类会重写父类的showCommon()方法
        showCommon();

        //invokevirtual 虚方法
        //info()是普通方法,有可能被重写,所以是虚方法
        info();

        MethodInterface in = null;
        //invokeinterface 虚方法
        in.methodA();
    }

    public void info() {

    }

    public void display(Father f) {

        f.showCommon();
    }

    public static void main(String[] args) {

        Son so = new Son();
        so.show();
    }
}

interface MethodInterface {

    void methodA();
}

2.2 关于invokedynamic指令:

  • JVM字节码指令集一直比较稳定,一直到Java7中才增加了一个invokedynamic指令,这是Java为了实现【动态类型语言】支持而做的一种改进
  • 但是在Java7中并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具来产生invokedynamic指令。直到Java8的 Lambda表达式 的出现,invokedynamic指令的生成,在Java中才有了直接的生成方式
  • Java7中增加的动态语言类型支持的本质是对Java虚拟机规范的修改,而不是对Java语言规则的修改,这一块相对来讲比较复杂,增加了虚拟机中的方法调用,最直接的受益者就是运行在Java平台的动态语言的编译器。
2.2.1 动态类型语言和静态类型语言

动态类型语言和静态类型语言两者的区别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之是动态类型语言。

说的再直白一点就是,静态类型语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征。

  • Java语言:String info = "mogu blog";(Java是静态类型语言的,编译进行类型检查)
  • JS语言:var name = "shkstart"; var name = 10;(运行时才进行检查)
  • Python语言:info = 130.5; (动态类型语言,编译时不知道,运行时候才知道是double类型的)
/**
 * 体会invokedynamic 指令
 */
@FunctionalInterface
interface Func {

    public boolean func(String str);
}

public class Lambda {

    public void lambda(Func func) {

        return;
    }

    public static void main(String[] args) {

        Lambda lambda = new Lambda();

        Func func = s -> {

            return true;
        };

        lambda.lambda(func);

        lambda.lambda(s -> {

            return true;
        });
    }
}

Func不知道是什么类型的,是根据返回值来确定的。
&nbsp;

3. 方法重写的本质

1、 找到操作数栈顶的第一个元素所执行的对象的实际类型,记作C;
2、 如果在类型C中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束,如果不通过,则返回java.lang.IllegalAccessError异常;
3、 否则,按照继承关系从下往上依次对C的各个父类进行第2步的搜索和验证过程;
4、 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常;

IllegalAccessError介绍
程序试图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。比如,你把应该有的jar包放从工程中拿走了,或者Mave
n中存在jar包冲突

4. 虚方法表

  • 在面向对象的编程中,会很频繁的使用到动态分派,如果在每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,JVM采用在类的方法区建立一个虚方法表(virtual method table)来实现,非虚方法不会出现在表中。使用索引表来代替查找
  • 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
  • 虚方法表是什么时候被创建的呢?
    虚方法表会在类加载的链接阶段被创建并开始初始化,类的变量初始值准备完成之后,JVM会把该类的虚方法表也初始化完毕。

4.1 示例一:

在子类son的虚方法表中,直接就指向Object的方法,而不需要先去找父类Father,再去找父类的父类。
&nbsp;

4.2 示例二:

可卡犬继承狗,可卡犬和猫实现Friendly接口
&nbsp;

4.2.1 代码:
public class VirtualMethodTable {

}

interface Friendly {

    void sayHello();
    void sayGoodbye();
}
class Dog {

    public void sayHello() {

    }
    @Override
    public String toString() {

        return "Dog";
    }
}

class Cat implements Friendly {

    public void eat() {

    }
    public void sayHello() {

    }
    public void sayGoodbye() {

    }
    protected void finalize() {

    }
    public String toString() {

        return "Cat";
    }
}

class CockerSpaniel extends Dog implements Friendly {

    public void sayHello() {

        super.sayHello();
    }
    public void sayGoodbye() {

    }
}
4.2.2 Dog的虚方法表:

重写toString方法和自己定义的sayHello,所以这2个方法在虚方法表中直接指向自己,没有重写的指向父类Object类中的方法,所以我们调用的时候直接就能找到对应类中的方法,而不需要一层层向上去寻找。
&nbsp;

4.2.3 可卡犬虚方法表

sayHello和sayGoodbay用的是自己重写后的方法,直接指向自己,toString()用的父类的,所以指向Dog中的toString方法,其余的都是指向Object类中。
&nbsp;

4.2.4 猫的虚方法表

猫中重写了图中7,10,11,12的方法,自己定义了finalize,这些指向自己,其他的指向Object
&nbsp;