虚拟机字节码执行引擎

 JAVA虚拟机规范中制定了虚拟机字节码执行引擎的概念模型,尽管现在JVM的实现各不相同,有编译执行(通过即时编译器产生本地代码执行,如BEA JRockit)也有解释执行(通过解释器执行,如Sun Classic VM)。但是从概念模型的角度来看,所有JAVA虚拟机的执行引擎都是一致的:输入的是字节码二进制流,处理过程是字节码解析的等效过程,输出的是执行结果

1. 运行时栈帧结构

  每个栈帧的内部存储这以下数据:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)(或表达式栈)
  • 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
  • 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
  • 一些附加信息
    栈帧的内部结构
     栈帧的大小取决于内部结构的大小。在编译java源程序的时候,栈帧中需要多大的局部变量表、需要多深的操作数栈都已经被计算出来,并且写入到了方法表的Code属性中。

1.1 局部变量表

  • 局部变量表又被称之为局部变量数组或本地变量表。
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型(8种)、对象引用(reference),以及returnAddress类型。
  • 由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
  • 局部变量表中的变量只在当前方法调用中有效。方法调用结束,方法栈帧出栈,局部变量表也会销毁。

实例:

1
2
3
4
5
6
7
8
9
public class LocalVariablesTest {
private int count = 0;

#14 public static void main(String[] args) {
#15 LocalVariablesTest test = new LocalVariablesTest();
#16 int num = 10;
#17// test.test1();
#18 }
}

 执行反汇编javap -v LocalVariablesTest.class得到字节码指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #3 // class com/hk7/memory/LocalVariablesTest
3: dup
4: invokespecial #4 // Method "<init>":()V
7: astore_1
8: bipush 10
10: istore_2
11: return
LineNumberTable:
line 15: 0
line 16: 8
line 18: 11
LocalVariableTable:
Start Length Slot Name Signature
0 12 0 args [Ljava/lang/String;
8 4 1 test Lcom/hk7/memory/LocalVariablesTest;
11 1 2 num I

 由上可知,locals=3代表局部变量表大小为3,分别为args参数、test对象和num变量。代码指令一共12行,LineNumberTable表示pc指令地址和源码所在行的对应关系。LocalVariableTable局部变量表,start表示当前变量的作用域,变量都是定义之后生效,比如test的start是8,从LineNumberTable表可知,8对应line16,也就是从16行开始test变量生效。length表示变量作用域的长度,从表中可看到三个变量起始位置+长度=12,因此所有变量作用域的结束都是方法的}。

关于Slot的理解

  • 1、参数值的存放总是从局部变量数组索引 0 的位置开始,到数组长度-1的索引结束。
  • 2、局部变量表,最基本的存储单元是Slot(变量槽),局部变量表中存放编译期可知的各种基本数据类型(8种)、引用类型、returnAddress类型的变量。
  • 3、在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型占用两个slot(1ong和double)。byte、short、char和boolean在存储前都会被转化成int。
  • 4、JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。占两个slot的变量使用起始索引访问。
    变量槽的索引访问
  • 5、当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个slot上
  • 6、如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。如下图jclasslib插件中可以看到字节码文件信息,test2位实例方法,它的this占了index=0的插槽。由于静态方法中栈帧局部变量表没有存储this变量,因此静态方法中无法使用this.xxx
    this变量存储

Slot的重复利用

局部变量槽是可以重复利用的,如果一个局部变量过了其作用域被销毁了,则下一个局部变量可以利用这个局部变量槽。比如以下代码:

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
36
37
38
39
40
41
42
43
public void test4() {
int a = 0;
{
int b = 0;
b = a + 1;
}
//b出了作用域,变量b被销毁,但是数组已经分配,因此c可以利用这个slot
//变量c使用之前已经销毁的变量b占据的slot的位置
int c = a + 1;
}



public void test4();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_1
5: iconst_1
6: iadd
7: istore_2
8: iload_1
9: iconst_1
10: iadd
11: istore_2
12: return
LineNumberTable:
line 36: 0
line 38: 2
line 39: 4
line 43: 8
line 44: 12
LocalVariableTable:
Start Length Slot Name Signature
4 4 2 b I
0 13 0 this Lcom/hk7/memory/LocalVariablesTest;
2 11 1 a I
12 1 2 c I

 局部变量表和性能调优密切相关,局部变量表占了栈帧的主要空间,因此它和栈溢出息息相关。局部变量表中的变量是垃圾回收的根节点,被局部变量表中的直接或间接引用的对象不会被回收。

1.2 操作数栈

 栈帧中的操作数栈Operand Stack采用数组来具体实现,也可被称之为表达式栈。操作数栈在方法的执行过程中,根据字节码指令,往栈中写入(入栈)或者提取(出栈)数据。
 操作数栈是栈(虚拟机栈)中栈(栈帧的操作数栈),它主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。局部变量表中的变量被操作的时候(比如执行i+j),会被压入操作数栈,经过一些指令(iadd)后再推出去,存储到局部变量表中。上述反汇编代码中的stack=2表示的是操作数栈的深度。

 栈中的任何一个元素都是可以任意的Java数据类型

  • 32bit的类型占用一个栈单位深度
  • 64bit的类型占用两个栈单位深度

操作数栈的具体实例可以看4.2

栈帧之间的数据共享

 一般情况下,两个栈帧之间的内存区域是独立的,但是JVM在实现过程中会进行一些优化,使两个栈帧之间共用一部分内存区域。

 两个栈帧之间数据共享,主要体现在方法调用中有参数传递的情况,上一个栈帧的部分局部变量表与下一个栈帧的操作数栈共用一部分空间,这样既节约了空间,也避免了参数的复制传递。

https://blog.csdn.net/weixin_45642014/article/details/110868636

https://www.cnblogs.com/snake23/archive/2019/01/28/10329149.html

栈顶缓存(Top-of-Stack-Cashing)技术

 jvm简单介绍文章里面提到过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候需要使用更多的入栈和出栈指令,操作数栈存储在内存中,因此会导致频繁的内存读写,影响执行速度。为了解决这个问题HotSpot的提出了栈顶缓存技术。
 栈顶缓存是指将栈顶元素全部缓存在物理cpu的寄存器中,以此降低对内存的读写次数,提升执行引擎执行效率。

1.3 动态链接

 栈帧里面的动态链接、方法返回地址和一些附加信息有些地方把它们统称为帧数据区
 动态链接又叫指向运行时常量池的方法引用

  • 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用区域

  • 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking),比如:invokedynamic指令

  • java源文件被编译为字节码文件后,所有的变量和方法的引用都会被保存到class文件的常量池(映射着的内存结构为方法区的运行时常量池)里面

  • 动态链接的作用是:把这些方法的符号引用转换为直接引用

 如下图所示,栈帧中有一个区域专门用来存储方法引用。方法区内存是多线程共享的,假设方法A和方法B都调用方法C,那么在运行时常量池会有方法C的方法引用,方法A和方法B对应的栈帧中的区域都会存储该方法引用。

动态链接

 以下代码,methodB()调用methodA(),反汇编之后的结果如下。从结果中我们可以看到,方法b中有指令5: invokevirtual #5,invokevirtual代表调用方法,#5为方法引用,表示常量池中的#5 = Methodref #28.#29

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
public class DynamicLink {

int num = 10;

public void methodA() {
System.out.println("methodA()....");
}

public void methodB() {
System.out.println("methodB()....");
methodA();
num++;
}

}

//javap -v xxx.class

public class com.hk7.memory.DynamicLink
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#23 // java/lang/Object."<init>":()V
#2 = Fieldref #8.#24 // com/hk7/memory/DynamicLink.num:I
#3 = Fieldref #25.#26 // java/lang/System.out:Ljava/io/PrintStream;
#4 = String #27 // methodA()....
#5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = String #30 // methodB()....
#7 = Methodref #8.#31 // com/hk7/memory/DynamicLink.methodA:()V
#8 = Class #32 // com/hk7/memory/DynamicLink
#9 = Class #33 // java/lang/Object
#10 = Utf8 num
#11 = Utf8 I
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 LocalVariableTable
#17 = Utf8 this
#18 = Utf8 Lcom/hk7/memory/DynamicLink;
#19 = Utf8 methodA
#20 = Utf8 methodB
#21 = Utf8 SourceFile
#22 = Utf8 DynamicLink.java
#23 = NameAndType #12:#13 // "<init>":()V
#24 = NameAndType #10:#11 // num:I
#25 = Class #34 // java/lang/System
#26 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#27 = Utf8 methodA()....
#28 = Class #37 // java/io/PrintStream
#29 = NameAndType #38:#39 // println:(Ljava/lang/String;)V
#30 = Utf8 methodB()....
#31 = NameAndType #19:#13 // methodA:()V
#32 = Utf8 com/hk7/memory/DynamicLink
#33 = Utf8 java/lang/Object
#34 = Utf8 java/lang/System
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Utf8 java/io/PrintStream
#38 = Utf8 println
#39 = Utf8 (Ljava/lang/String;)V
{
int num;
descriptor: I
flags:

public com.hk7.memory.DynamicLink();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush 10
7: putfield #2 // Field num:I
10: return
LineNumberTable:
line 9: 0
line 11: 4
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/hk7/memory/DynamicLink;

public void methodA();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String methodA()....
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 14: 0
line 15: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/hk7/memory/DynamicLink;

public void methodB();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #6 // String methodB()....
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: aload_0
9: invokevirtual #7 // Method methodA:()V
12: aload_0
13: dup
14: getfield #2 // Field num:I
17: iconst_1
18: iadd
19: putfield #2 // Field num:I
22: return
LineNumberTable:
line 18: 0
line 19: 8
line 20: 12
line 21: 22
LocalVariableTable:
Start Length Slot Name Signature
0 23 0 this Lcom/hk7/memory/DynamicLink;
}

1.4 方法返回地址

 当方法开始执行后,只有2种方式退出该方法:

  1. 遇到return等字节码指令,正常退出,正常调用完成。
  2. 遇到异常athrow指令,异常退出,异常调用完成。

 无论哪一种方式退出,都必须返回到方法被调用的位置。正常退出时,pc计数器的值作为返回地址,栈帧中会保存这个值;异常退出时,返回地址通过异常处理器表来确定,栈帧中不会保存该部分值。

 方法退出等同于栈帧出栈,因此退出时,需要执行的操作包括:恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数栈中,调整pc计数器的值以指向调用方下一条指令等。

1.5 一些附加信息

 《java虚拟机规范》中没有描述到的一些信息也可以保存在栈帧之中。比如调试、性能搜集等相关信息,取决于虚拟机具体的实现。

2. 方法调用

2.1 解析

 在类加载阶段,class文件里面存储的都是符号引用,在类加载的解析阶段,如果被调用的目标方法在编译期可知,且运行期保持不变时,则会直接将方法的符号引用转化为直接引用。这类方法的调用被称为解析(Resolution)。

 满足编译期可知,运行期不变的方法有:静态方法、私有方法、实力构造器、父类方法、+ final修饰的方法(由invokevirtual指令调用)。这5种方法会在类加载的时候就把符号引用解析为直接引用,统称为“非虚方法”,除此之外的称为“虚方法

1
2
3
4
5
6
invokestatic   			调用静态方法
invokespecial 调用构造器<init>()方法、私有方法和父类中的方法

invokevirtual 调用所有的虚方法
invokeinterface 调用接口方法,运行时再确定一个实现实现该接口的对象
invokedynamic 运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。

 查看以下say方法的调用,

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public static void main(String[] args) throws InterruptedException {
say();
}

public static void say(){
System.out.println("hello");
}

//javap -v Test.class
{
public com.hk7.memory.StackFrameTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/hk7/memory/StackFrameTest;

public static void main(java.lang.String[]) throws java.lang.InterruptedException;
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: invokestatic #2 // Method say:()V LOOK THIS
3: return
LineNumberTable:
line 14: 0
line 15: 3
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 args [Ljava/lang/String;
Exceptions:
throws java.lang.InterruptedException

public static void say();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String hello
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 18: 0
line 19: 8
}

 解析调用是一个静态的过程。

2.2 分派

 和解析调用想对应的是分派调用,它可能是静态的,也可能是动态的。按照分派的宗量数可以分为单分派和多分派。以上2种分类方式组合,构成了静态单分派、静态多分派、动态单分派和动态多分派四种。

静态分派

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
public class StaticDispatchTest {

static abstract class Human {
}

static class Man extends Human {
}

static class Woman extends Human {

}

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

public void say(Man man) {
System.out.println("Hello, gentleman!");
}

public void say(Woman woman) {
System.out.println("Hello, lady!");
}

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

StaticDispatchTest sd = new StaticDispatchTest();
sd.say(man); //Hello, guy!
sd.say(woman); //Hello, guy!
}
}

 先说说2个概念。

 Human叫做静态类型(Static Type)或者外观类型(Apparent Type),Man和Woman叫做实际类型(Actual Type)或者运行时类型(Runtime Type)。

 静态类型和实际类类型在运行中都可能发生变化,区别在于静态类型的变化仅仅在使用中发生,变量本身的静态类型不会发生变化,且最终的静态类型在编译时可知。

 实例类型变化的结果在运行期才可知,编译前并不会知晓。

1
2
3
4
5
6
//实际类型变化,a b的值可能变化,无法确定到底是哪一种类型
Human human = (a == b) ? new Woman() : new Man();

//静态类型变化,强制转换,编译期间可以确定
sd.say((Woman) human);
sd.say((Man) human);

 以上代码,manwoman两个变量静态类型相同、实际类型不同。虚拟机在重载时,通过参数的静态类型来作为判定依据。静态类型在编译期可知,因此在javac编译时,选择了say(Human)作为调用目标,并把这个方法的符号引用写到main()方法里面的两条invokevirtual指令的参数中。

所有依赖静态类型来决定方法执行版本的分派动作,都叫做静态分派,最典型的引用就是方法的重载。静态分派动作发生在编译期,而不是由虚拟机来执行的,因此有些资料把它归入“解析”,而不是“分派”。

 javac虽然能匹配到方法的版本,但有时候并不是唯一的,它只能匹配出“相对更合适的”方法。比如以下代码:重载方法匹配优先级

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
36
37
38
39
40
41
42
43
44
45
46
47
public class Overload {
// Object 参数
public static void say(Object arg) {
System.out.println("hello object");
}
// int 参数
public static void say(int arg) {
System.out.println("hello int");
}
// long 参数
public static void say(long arg) {
System.out.println("hello long");
}
// char 参数
public static void say(char arg) {
System.out.println("hello char");
}

// Character 参数
public static void say(Character arg) {
System.out.println("hello character");
}
// 变长参数
public static void say(char... arg) {
System.out.println("hello char...");
}

// Serializable 参数
public static void say(Serializable arg) {
System.out.println("hello serializable");
}
public static void main(String[] args) {
say('a'); //char、int、
}
}


/**
假设依次注释掉最匹配的方法,代码输出如下,
char => 'hello char'
int => 'hello int' 此处发生了类型转换,'a' => 97
long => 'hello long' 'a' => int 97 => long 97
Character => 'hello Character' 此时发生了装箱行为,'a' => java.lang.Character
Serializable => 'hello Serializable' 装箱后发现Character没找到,但是找到了它的接口类型. public final class Character implements java.io.Serializable, Comparable<Character> {
Object => 'hello Object' 装箱后,父类为Object
char... => 'hello char...' 变长参数的重载优先级是最低的。
**/

 注意,Character实现了多个接口,如果定义了多个接口重载方法,那么编译器无法选择,会提示“Type Ambiguous”并拒绝编译。此时必须显示指定,比如say(Comparable<Character>'a')。继承父类,会按照由子到父的顺序逐层转换。

动态分派

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
public class StaticDispatchTest {

static abstract class Human {
protected abstract void say();
}

static class Man extends Human {
@Override
protected void say() {
System.out.println("Hello, gentleman!");
}
}

static class Woman extends Human {

@Override
protected void say() {
System.out.println("Hello, lady!");
}
}

public static void main(String[] args) {
Human man = new Man();
Human woman = new Woman();
man.say();//hello, gentleman
woman.say();//hello, lady
man = new Woman();
man.say();//hello, lady
}
}

 从以上代码输出,我们可以猜测,重写的实质是根据方法实际类型来选择方法版本

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
//javap -v StaticDispatchTest.class
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2 // class com/hk7/memory/DynamicDispatchTest$Man
3: dup /*复制操作数栈顶元素再入栈,因为invokesepcial执行init会使用到*/
4: invokespecial #3 // Method com/hk7/memory/DynamicDispatchTest$Man."<init>":()V
7: astore_1
8: new #4 // class com/hk7/memory/DynamicDispatchTest$Woman
11: dup
12: invokespecial #5 // Method com/hk7/memory/DynamicDispatchTest$Woman."<init>":()V
15: astore_2
16: aload_1
17: invokevirtual #6 // Method com/hk7/memory/DynamicDispatchTest$Human.say:()V
20: aload_2
21: invokevirtual #6 // Method com/hk7/memory/DynamicDispatchTest$Human.say:()V
24: new #4 // class com/hk7/memory/DynamicDispatchTest$Woman
27: dup
28: invokespecial #5 // Method com/hk7/memory/DynamicDispatchTest$Woman."<init>":()V
31: astore_1
32: aload_1
33: invokevirtual #6 // Method com/hk7/memory/DynamicDispatchTest$Human.say:()V
36: return

 分析以上字节码代码,#0~#15分别实例化了2个对象,#16#20行aload指令分别把刚刚创建的两个对象的引用压入栈顶,这两个对象是即将要执行say()方法的所有者,称为”接收者“。#17#20指令分别调用方法say,虽然他们的指令和参数一样,但是最终的执行的目标方法不同。invokevirtual指令的运行时解析过程如下:

1
2
3
4
1. 找到操作数栈顶的第一个元素指向的实际类型,记作C。
2. 如果在类型C中找到与常量中的描述符和简单名称都相同的方法,则进行权限校验,如果校验通过则返回这个方法的直接引用,查找结束。不通过则java.lang.IllegalAccessError异常。
3. 找不到则按照继承关系,从下往上依次对C的父类进行步骤2的搜索。
4. 如果一直没找到,则java.lang.AbstractMethodError异常。

invokevirtual指令会在运行期确定接收者的实际类型,进行确定方法的版本,这就是java中方法重写的本质。

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
36
public class FieldHasNoPolymorphic {

static class Father {
public int money;

public Father() {
money = 2;
show();
}

public void show() {
System.out.println("I am father, i have $" + money);
}
}

static class Son extends Father {
public int money;

public Son() {
money = 4;
show();
}

public void show() {
System.out.println("I am son, i have $" + money);
//I am son, i have $0
//I am son, i have $4
//this gay has $2
}
}

public static void main(String[] args) {
Father gay = new Son();
System.out.println("this gay has $" + gay.money);
}
}

new Son()会先隐式调用父类的构造方法,因此Father的money被初始化为2。然后调用show时,调用son的show(),打印出I am son, i have $0,然后父类构造方法结束后会调用自己的构造方法,此时先赋值,再输出I am son, i have $4gay.money字段并不存在多态性,因此直接访问gay的静态类型的值输出this gay has $2

单分派和多分派

 方法的接收者和方法的参数统称为宗量,分派时选择一个总量叫做单分派,选择>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
36
public class SingleMultiDispatch {
static class QQ {
}

static class _360 {
}

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

public void hardChoice(_360 arg) {
System.out.println("father choose _360");
}
}

public static class Son extends Father {
@Override
public void hardChoice(QQ arg) {
System.out.println("son choose QQ");
}

@Override
public void hardChoice(_360 arg) {
System.out.println("son choose 360");
}
}

public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
#1 father.hardChoice(new _360());//father choose 360
#2 son.hardChoice(new QQ());//son choose QQ
}
}

目前Java语言的静态分派属于多分派类型;动态分派属于单分派类型;

 先看静态分派过程,选择目标有2个:静态类型是Father还是Son,参数是QQ还是360,代码#1#2在静态编译中产生了两条invokevirtual指令,两条指令的参数分别指向常量池中的Father::hardChoice(360)和Father::hardChoice(QQ)。
 再看动态运行时虚拟机的选择,也就是动态分派过程,此时选择目标只有1个,即实际类型。在执行#1时,发生重载,直接按照静态类型选择Father的参数为360的方法。在执行#2时,发生了重写,此时按照son的实际类型进行选择,因此选择Son的hardChoice方法,而选择哪一个,在静态分派过程已经选择好了,执行Son::hardChoice(QQ)。

虚拟机动态分派的实现

  由于动态分派是非常频繁的操作,而且动态分派的方法版本选择过程需要运行时在类的方法元数据中搜索合适的目标方法,虚拟机不可能每次都去搜索,因此在虚拟机的实际实现中,使用了为类型在方法区中建立一个虚方法表的方式。(invokevirtual 指令会搜索vtable,invokeinterface指令会搜索itable)

3. 动态类型语言支持

变量无类型而变量值才有类型,比如php、python、ruby、javascript等。这种被称作动态类型语言

3.1 java和动态类型

 在jdk7以前,调用方法的指令有4种invokevirtual invokespecial invokestatic invokeinterface,这4中指令的第一个参数都是被调用的方法的符号引用。但是动态类型在编译期是无法确定的,只有运行到该出才知道是什么类型,要处理这种情况很麻烦,为了解决这个问题,jdk7引入了新的指令invokedynamic指令以及java.lang.invoke包。

3.2 java.lang.invoke

 java.lang.invoke包是jdk7引入的包,主要目的是为了在之前单纯依靠符号引用来确定调用的目标方法这种方式之外,提供一种新的动态确定目标方法的机制名称为”方法句柄(Method Handle)“。

 方法句柄类似于C的函数指针。 void sort(int list[], const int size, int (*compare)(int, int)),java中无法单独把函数作为参数传递,一般都是设计带有compare()方法的接口,比如sort(list, Comparator c)

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 com.hk7.jvm;

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

import static java.lang.invoke.MethodHandles.lookup;

public class MethodHandleTest {

static class ClassA {
public void println(String x) {
System.out.println("classA:" + x);
}
}

public static void main(String[] args) throws Throwable {
//如果当前时间是2的倍数,则调用系统System.out.Println方法,否则调用ClassA的Println方法方法。
Object obj = System.currentTimeMillis() % 2 == 0 ? System.out : new ClassA();
getMethodHandle(obj).invokeExact("test");//调用对象的方法
}

private static MethodHandle getMethodHandle(Object receiver) throws Throwable {
//方法的返回值和参数(参数可以有多个,从第2个参数开始算起)
MethodType mt = MethodType.methodType(void.class, String.class);
//lookup():在指定类中查找符合给定的方法方法名称、方法类型,并且符合权限的方法句柄
//findVirtual():生成方法虚方法的方法句柄。
//被调用的方法的接受者(类或者接口)、方法名、方法的返回值和参数
//调用receiver的println方法,参数为String,返回值为空,即调用 public void println(String x){}方法
MethodHandle println = lookup().findVirtual(receiver.getClass(), "println", mt);
//bindTo:绑定方法句柄的第一个参数到receiver上,但是不调用它
return println.bindTo(receiver);
}
}

 但是有了MethodHandler之后则可以实现类似于C/C++函数传递了,void sort(List list, MethodHandle compare)

 从以上代码可以看出,MethodHandle和Reflection类似,但是二者是有区别的。

  • MethodHandle和Reflection本质都是模拟方法的调用,但是Reflection是模拟代码层面的调用,MethodHandle则是字节码层面的调用。MethodHandles.Lookup中的3个方法findStatic() findVirtual() findSpecial()对应于字节码指令invokestatic invokevirtual/invokeinterface invokespecial,这些底层细节在反射中并不关心。
  • Reflection包含的信息比MethodHandle多得多,它是方法在java端的全面映像。Reflection是重量级、MethodHandle是轻量级。
  • MethodHandle是对字节码方法指令的模拟,因此理论上虚拟机对字节码的优化等,MethodHandle也应该实现。但是发射调用不可能执行优化措施。
  • Reflection是为java语言服务的,MethodHandle则可以服务于所有基于虚拟机的语言。

3.3 invokedynamic

 invokedynamic指令与MethodHandle机制的作用是一样的,都是为了解决原有4条“invoke*”指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包含其他语言的设计者)有更高的自由度。它们两者的思路也是可类比的,可以把它们想象成为了达成同一个目的,一个采用上层Java代码和API来实现,另一个用字节码和Class中其他属性、常量来完成。

1
2
3
4
5
invokestatic   //调用静态方法
invokespecial  //调用私有方法、实例构造器方法、父类方法
invokevirtual  //调用实例方法
invokeinterface //调用接口方法,会在运行时再确定一个实现此接口的对象
invokedynamic  //先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法,在此之前的4条调用指令,分派逻辑是固化在java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。

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

​ 详情看文章https://www.cnblogs.com/sheeva/p/6344388.html,讲的不错。

3.4 掌握方法分派实战

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
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.hk7.jvm;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Field;

import static java.lang.invoke.MethodHandles.lookup;

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

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

public class Son extends Father {
public void thinking() {
// 请在这里填入适当的代码(不能修改其他地方的代码)
// 实现调用祖父类的thinking() 方法,打印 "i am grandfather"
try {
//这种在jdk7之后,只能访问到father,因为有访问限制
// MethodType mt = MethodType.methodType(void.class);
// MethodHandle mh = lookup().findSpecial(GrandFather.class, "thinking", mt, getClass());
// mh.invokeExact(this);

MethodType mt = MethodType.methodType(void.class);
Field lookupImpl = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
lookupImpl.setAccessible(true);
MethodHandle mh = ((MethodHandles.Lookup) lookupImpl.get(null)).findSpecial(GrandFather.class, "thinking", mt, getClass());
mh.invokeExact(this);
} catch (Throwable e) {
e.printStackTrace();
}
}
}


public static void main(String[] args) {
(new MethodDispatchTest().new Son()).thinking();
}
}

4. 基于栈的字节码解释执行引擎

 前面讲的是虚拟机如何调用方法、进行方法版本的选择。接下来本节讨论虚拟机是如何执行方法里面的字节码指令的。

 java虚拟机的执行引擎在执行java代码的时候有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)。

4.1 解释执行

 不论是物理机还是虚拟机,大部分的程序代码从开始编译到最终转化成物理机的目标代码或虚拟机能执行的指令集之前,都会按照如下图所示的各个步骤进行。其中绿色的模块可以选择性实现,中间的那条分支是解释执行的过程(即一条字节码一条字节码地解释执行,如JavaScript),而下面的那条分支就是传统编译原理中从源代码到目标机器代码的生成过程。

编译过程

 对于具体的语言实现而言,词法、语法分析直至优化器和目标代码生成器,可以独立于执行引擎,形成一个完整的编译器去执行,比如C/C++。也可以把其中部分实现为半独立的编译器,比如Java。也可以把这些步骤和执行引擎集中在封闭的黑匣子之中,如多数JavaScript执行引擎。

 Java语言中,Javac编译器完成了程序代码~字节码指令流的不部分。这一部分是在java虚拟机之外进行的,而解释器在虚拟机内部,所以Java语言的编译是半独立的实现。

4.2 基于栈的解释器执行过程

代码追踪实例

1
2
3
4
5
public static void main(String[] args) {
byte i = 15;
int j = 800;
int k = i + j;
}

javap -v xxx.class之后得到字节码指令

1
2
3
4
5
6
7
8
9
10
stack=2, locals=4, args_size=1
0: bipush 15
2: istore_1
3: sipush 800
6: istore_2
7: iload_1
8: iload_2
9: iadd
10: istore_3
11: return

 以上代码的具体执行如下图,指令类型采用所能包含的值的最小整数类型指令

    1. 当执行main函数时,创建好栈帧。pc寄存器的值为0,栈帧中初始化了局部变量表和操作数栈,值为空
    1. 执行地址为0的指令,bipush,pc=2,15入栈
    1. 执行地址为2的指令,istore_1,pc=3,15出栈,局部变量表slot1=15(实例方法,slot0存储的是this变量)
    1. 执行地址为3的指令,sipush(由于800超过了byte的范围,所以会使用sipush,short类型),pc=5,8入栈
    1. 执行地址为5的指令,istore_2,pc=6,8出栈,局部变量表slot2=8
    1. 执行地址为6的指令,iload_1,pc=7,从slot1中加载15,15入栈
    1. 执行地址为7的指令,iload_2,pc=8,从slot2中加载8,8入栈
    1. 执行地址为8的指令,iadd,pc=9,8和15出栈并执行相加操作,23入栈
    1. 执行地址为9的指令,istore_3,pc=10,23出栈,局部变量表slot3=23
    1. 执行地址为10的指令,return。函数结束,栈帧销毁。
      代码追踪

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!