前言

疫情期间看完了《深入理解Java虚拟机》一书,看完之后觉得有些囫囵吞枣,没有留下深刻印象,因此写点小小总结,把握一下重点知识,尽量形成认知框架中的一部分。

原书的内容十分详细,也肯定写得比我好。我这里只做简单的概括,详细的还是去书里看比较好。

方法调用

前面已经了解了Class文件的组成结构,了解了字节码的组成方式以及被加载到内存中的过程,并且虚拟机的运行时数据区域结构也已经描述过,接下来了解虚拟机的方法调用过程

方法调用过程并不是代码执行过程,而是指如何确定被调用方法的版本的过程

按之前的了解,Class文件中的常量池一项中记录了类相关的符号引用,类加载之后就保存在运行时常量池中。

这些符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接

解析

类加载的解析阶段,有部分符号引用会转化成直接引用,这类方法的特点是:方法在程序运行之前就有一个可确定且唯一的版本,并且这个方法的调用版本在运行期不会被改变

能被invokestaticinvokespecial指令调用的方法,都满足以上特点,因此可以在解析阶段转换,更准确的说是以下5类方法:

  • 静态方法
  • 私有方法
  • 实例构造器<init>()方法
  • 父类方法
  • final修饰的方法(它使用invokevirtual指令调用)

这些方法统称为“非虚方法”(Non-Virtual Method),与之相反,其他方法就被称为“虚方法”(Virtual Method)

解析调用一定是个静态的过程,在编译期间就完全确定,在类加载的解析阶段就会把涉及的符号引用全部转变为明确的直接引用,不必延迟到运行期再去完成。

分派

分派(Dispatch)调用复杂许多,它可能是静态的也可能是动态的,按照分派依据的宗量数可分为单分派和多分派。这两类分派方式两两组合就构成了静态单分派、静态多分派、动态单分派、动态多分派4种分派组合情况

1.静态分派

所有依赖静态类型来决定方法执行版本的分派动作,都称为静态分派。静态分派最典型应用表现就是方法重载

首先解释静态类型和实际类型:

  • 静态类型:静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的
  • 实际类型:实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package org.fenixsoft.polymorphic;

/**
* 方法静态分派演示
* @author zzm
*/
public class StaticDispatch {

static abstract class Human { }
static class Man extends Human { }
static class Woman extends Human { }

public void sayHello(Human guy) {
System.out.println("hello,guy!");
}

public void sayHello(Man guy) {
System.out.println("hello,gentleman!");
}

public void sayHello(Woman guy) {
System.out.println("hello,lady!");
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.sayHello(man); // 1
sr.sayHello(woman); // 2

sr.sayHello((Man)man); // 3
sr.sayHello((Woman)woman); // 4
}
}

如上,对于变量man,它的静态类型是Human,而它的实际类型是Man。编译器在编译时可以确定的是静态类型(因为实际类型有可能是Man或者Woman),所以静态分派只会根据变量的静态类型确定调用的方法类型。

以上代码的输出:

1
2
3
4
hello,guy!
hello,guy!
hello,gentleman!
hello,lady!

注意:Javac编译器虽然能确定出方法的重载版本,但在很多情况下这个重载版本并不是“唯一”的,往往只能确定一个“相对更合适的”版本。(详细案例见书本8.3.2节)

2.动态分派

在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。

这与面向对象的重写对应,Java的多态就体现在这。

这很类似于c++中的虚函数调用

invokevirtual指令先找到对象的实际类型,然后查找它的方法是否存在符合的,如果没有,就去查找其父类,以此类推。因此,实际类型在这里十分关键。

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package org.fenixsoft.polymorphic;

/**
* 方法动态分派演示
* @author zzm
*/
public class DynamicDispatch {
static abstract class Human {
protected abstract void sayHello();
}

static class Man extends Human {
@Override
protected void sayHello() {
System.out.println("man say hello");
}
}

static class Woman extends Human {
@Override
protected void sayHello() {
System.out.println("woman say hello");
}
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.sayHello();
woman.sayHello();
man = new Woman();
man.sayHello();
}
}

输出如下:
1
2
3
man say hello
woman say hello
woman say hello

书上的第2个例子很有趣,值得一看,这里不再赘述

动态分派通过在类的方法区中建立虚方法表(Virtual Method Table,也称为vtable)来实现。虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。

这与c++很类似

方法表结构示意图如下:


方法表结构

3.单分派与多分派

方法调用者和方法参数都是宗量

Java中静态分派的方法调用,首先确定调用者的静态类型是什么,然后根据要调用的方法参数的静态类型(声明类型)确定所有重载方法中要调用哪一个,需要根据这两个宗量来编译,所以是静态分派是多分派

Java中动态分派的方法调用,在运行期间,虚拟机会根据调用者的实际类型调用对应的方法,秩序根据这一个宗量就可以确定要调用的方法,所以动态分派是单分派

动态类型语言支持

动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译期进行的,满足这个特征的语言有很多,常用的包括:APL、Clojure、Erlang、Groovy、 JavaScript、Lisp、Lua、PHP、Prolog、Python、Ruby、Smalltalk、Tcl等等

静态类型语言与动态类型语言的优缺点:

  • 静态类型语言能够在编译期确定变量类型,最显著的好处是编译器可以提供全面严谨的类型检查,这样与数据类型相关的潜在问题就能在编码时被及时发现,利于稳定性及让项目容易达到更大的规模。
  • 动态类型语言在运行期才确定类型,这可以为开发人员提供极大的灵活性,某些在静态类型语言中要花大量臃肿代码来实现的功能,由动态类型语言去做可能会很清晰简洁,清晰简洁通常也就意味着开发效率的提升。

Java并不是动态类型语言,因此接下来要说的invokedynamic指令貌似与平常的使用无关,是Java虚拟机为支持其他语言在虚拟机上的使用而做的实现

invokedynamic指令具体的示例细节见8.4.4节,这里不赘述了

每一处含有invokedynamic指令的位置都被称作“动态调用点(Dynamically-Computed Call Site)”,这条指令的第一个参数不再是代表方法符号引用的CONSTANT_Methodref_info常量,而是变为JDK 7时新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可以得到3项信息:引导方法(BootstrapMethod,该方法存放在新增的BootstrapMethods属性中)、方法类型(MethodType)和名称。引导方法是有固定的参数,并且返回值规定是java.lang.invoke.CallSite对象,这个对象代表了真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并且执行引导方法,从而获得一个CallSite对象,最终调用到要执行的目标方法上。