运行时数据区2

虚拟机栈

背景

https://www.kylin.show/62861.html

由于跨平台性的设计,java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。

根据栈设计的优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。

内存中的堆与栈

栈是运行时的单位,而堆是存储的单位

  1. 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
  2. 一般来讲,对象主要都是放在堆空间的,是运行时数据区比较大的一块
  3. 栈空间存放 基本数据类型的局部变量,以及引用数据类型的对象的引用

特点

java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的java方法调用。它是线程私有。

虚拟机栈 栈帧

生命周期和线程是一致的

栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器(程序计数器)

作用:主管java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。

  • 局部变量:相较于成员变量(成员变量或称属性)
  • 基本数据变量: 相对于引用类型变量(类,数组,接口)

JVM直接对java栈的操作只有两个

  • 每个方法执行,伴随着进栈(入栈,压栈)
  • 执行结束后的出栈工作

对于栈来说不存在垃圾回收问题,但是肯定存在出现OOM异常(栈空间不足)

虚拟机栈压出栈

栈中可能出现的异常

java虚拟机规范允许Java栈的大小是动态的或者是固定不变的

  • 如果采用固定大小的Java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机将会抛出一个 StackOverFlowError异常
  • 如果java虚拟机栈可以动态拓展,并且在尝试拓展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个 OutOfMemoryError异常

image-20201005202743560

1
2
3
4
5
6
7
8
public class StackErrorTest {
private static int count = 1;

public static void main(String[] args) {
////Exception in thread "main" java.lang.StackOverflowError
main(args);
}
}

StackOverflowError

栈帧数量超过了虚拟机栈大小,就会出现java.lang.StackOverflowError.

设置栈的内存大小

我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。

https://docs.oracle.com/en/java/javase/11/tools/java.html#GUID-3B1CE181-CD30-4178-9602-230B800D4FAE

image-20201005203808193

修改一下我们先前的代码。定义一个变量count,打印后自增1。

image-20201005203927248

此时运行打印count为10824后出现java.lang.StackOverflowError错误。

接着在IDEA中修改虚拟机参数。通过-Xss修改虚拟机栈大小为256k

image-20201005204215028

再次运行程序,此时打印到1874,就出现了错误。说明虚拟机栈的大小的确变小了!!

image-20201005204335187

栈的存储单位

每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在的。

在这个线程上正在执行的每个方法都各自对应一个 栈帧(Stack Frame).

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循先进后出/后进先出的和原则。

在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Frame)

执行引擎运行的所有字节码指令只针对当前栈帧进行操作

如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。

虚拟机栈栈帧

image-20201005213728226

image-20201005213745011

不同线程中所包含的栈帧是不允许相互引用的,即不可能在另一个栈帧中引用另外一个线程的栈帧

如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。

Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常(未被处理异常)。不管使用哪种方式,都会导致栈帧被弹出。

image-20201005215644620

使用try/catch处理异常,相当于正常函数return返回。

image-20201005215850828

使用java -v查看字节码文件

image-20201005220114809

void都是用return返回,int型是ireturn,double型是dreturn

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

/**
* 方法的结束方式分为两种:① 正常结束,以return为代表 ② 方法执行中出现未捕获处理的异常,以抛出异常的方式结束
*/
public class StackFrameTest {
public static void main(String[] args) {
try {
StackFrameTest test = new StackFrameTest();
test.method1();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("main()正常结束");
}

public void method1() {
System.out.println("method1()开始执行...");
method2();
System.out.println("method1()执行结束...");
System.out.println(10 / 0);

//return ;//可以省略
}

public int method2() {
System.out.println("method2()开始执行...");
int i = 10;
int m = (int) method3();
System.out.println("method2()即将结束...");
return i + m;
}

public double method3() {
System.out.println("method3()开始执行...");
double j = 20.0;
System.out.println("method3()即将结束...");
return j;
}
}

栈帧的内部结构

每个栈帧中存储着:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)(或表达式栈)
  • 动态链接(Dynamic Linking)(或执行运行时常量池的方法引用)—-深入理解Java多态特性必读!!
  • 方法返回地址(Return Adress)(或方法正常退出或者异常退出的定义)
  • 一些附加信息

栈帧的内部结构

局部变量表-LV

  • 局部变量表也被称之为局部变量数组或本地变量表
  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddressleixing
  • 由于局部变量表是建立在线程的栈上,是线程私有的数据,因此不存在数据安全问题
  • 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
  • 方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。
  • 局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

代码验证

image-20201006093138606

我们首先使用编译后使用javap -v 查看字节码文件。查看main方法中的LV表

image-20201006093504777

字节码方法内部剖析

我们也可以使用jclasslib插件查看字节码文件

image-20201006093943233

接着我们来详细查看

image-20201006095300304

值得注意的是L代表引用类型数据。数组用了[。括号()代表的是方法的参数。例如String类型参数,和String数组参数

String[]:([Ljava/lang/String;)

String:(Ljava/lang/String);

接下来查看Methods-方法-Code中的的细节

image-20201006095930156

image-20201006100007118

image-20201006100357373

接下来查看Methods-方法-Code-LineNumberTable中的的细节

image-20201006100659856

image-20201006100921012

接下来查看Methods-方法-Code-LocalVariableTable中的的细节,也就是我们的局部变量表。

image-20201006103030503

Slot理解

1.参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束

2.局部变量表,最基本的存储单元是Slot(变量槽)

3.局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。

4.在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型)引用类型也是占用一个,64位的类型(long和double)占用两个slot。

byte、short、char、float在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true;

long和double则占据两个slot。

5.JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值

6.当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照声明顺序被复制到局部变量表中的每一个slot上

7.如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或者double类型变量)

8.如果当前帧是由构造方法或者实例方法(非静态 )创建的(意思是当前帧所对应的方法是构造器方法或者是普通的实例方法),那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序排列。

局部变量表插槽slot

第8点也就解释了为啥静态方法不能使用this。因为this变量不存在于当前方法的局部变量表中。

image-20201006105328014

我们通过插件查看字节码文件进行验证。

image-20201006105925231

image-20201006105629647

的确可以发现构造方法或者实例方法(非静态 )创建的栈帧中,局部变量表中this将会存放在index为0的slot处,其余的参数按照参数表顺序排列。

slot的重复利用

栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

image-20201006110257629

这是因为slot重复利用了。

image-20201006110957513

  • 在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
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
public class LocalVariablesTest {
private int count = 0;

public static void main(String[] args) {
LocalVariablesTest test = new LocalVariablesTest();
int num = 10;
test.test1();
}

//练习:
public static void testStatic() {
LocalVariablesTest test = new LocalVariablesTest();
Date date = new Date();
int count = 10;
System.out.println(count);
//因为this变量不存在于当前方法的局部变量表中!!
//System.out.println(this.count);
}

//关于Slot的使用的理解
public LocalVariablesTest() {
this.count = 1;
}

public void test1() {
Date date = new Date();
String name1 = "kylin.show";
test2(date, name1);
System.out.println(date + name1);
}

public String test2(Date dateP, String name2) {
dateP = null;
name2 = "kylin";
double weight = 130.5;//占据两个slot
char gender = '男';
return dateP + name2;
}

public void test3() {
this.count++;
}

public void test4() {
int a = 0;
{
int b = 0;
b = a + 1;
}
//变量c使用之前已经销毁的变量b占据的slot的位置
int c = a + 1;
}

/*
变量的分类:按照数据类型分:① 基本数据类型 ② 引用数据类型
按照在类中声明的位置分:① 成员变量:在使用前,都经历过默认初始化赋值
类变量: linking的prepare阶段:给类变量默认赋值 ---> initial阶段:给类变量显式赋值即静态代码块赋值
实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值
② 局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过
*/
public void test5Temp() {
int num;
//System.out.println(num);//错误信息:变量num未进行初始化 Variable 'num' might not have been initialized
}

}

操作数栈-OS

每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以成为表达式栈。

操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)或出栈(pop)

某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈,使用他们后再把结果压入栈。(如字节码指令bipush操作)

比如:执行复制、交换、求和等操作

image-20201006142845937

操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。

操作数栈就是jvm执行引擎的一个工作区,当一个方法开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的(数组已经创建了 有长度了)

每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译器就定义好了,保存在方法的code属性中,为max_stack的值。

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

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

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈push和出栈pop操作来完成一次数据访问

如果被调用的方法带有返回值的话,其返回值将会被压人当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译器期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段要再次验证。

另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

image-20201006143950222

代码验证

image-20201006144726634

1
2
3
4
5
6
7
8
9
 0 bipush 15
2 istore_1
3 bipush 8
5 istore_2
6 iload_1
7 iload_2
8 iadd
9 istore_3
10 return

byte short char boolean int :都以int型来保存

PC寄存器中存放的是0,执行0地址的指令bipush 15 15压人操作数栈中,PC寄存器指变为2执行istore_1将操作数栈中的15存储到局部变量表中索引为1的位置。(0存放的是this,省略了)

image-20201006145058678

PC寄存器中存放的是3,执行3地址的指令bipush 8 8压人操作数栈中,PC寄存器指变为5执行`istore_2将操作数栈中的8存储到局部变量表中索引为2的位置。

PC寄存器值为6,执行iload_1将 局部变量表中索引为1的值15取出存放到操作数栈中。PC寄存器值为7,执行iload_2将 局部变量表中索引为2的值8取出存放到操作数栈中。

iload

PC寄存器值为8,执行iadd值,将操作数栈中的值给相加8+15=23,PC寄存器值为9执行istore_3将操作数栈中的值23,存放到局部变量表索引为3的位置。最后return弹出栈帧。

如果被调用的方法带有返回值的话,其返回值将会被压人当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。

image-20201006151025369

栈顶缓存技术

基于栈式架构的虚拟机所使用的零地址指令(即不考虑地址,单纯入栈出栈)更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数

由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率

image-20201006154529695

动态链接-DL

帧数据区

方法返回地址,动态链接,一些附加信息,又被统称为帧数据区。

每一个栈帧内部都包含一个指向运行时常量池Constant pool或该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令

在Java源文件被编译成字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Refenrence)保存在class字节码文件(javap反编译查看)的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用(#)最终转换为调用方法的直接引用。

image-20201006160306004

image-20201006160344697

image-20201006160535124

运行时常量池位于方法区(注意: JDK1.7 及之后版本的 JVM 已经将运行时常量池从方法区中移了出来,在 Java 堆(Heap)中开辟了一块区域存放运行时常量池。)

为什么需要常量池呢?
常量池的作用,就是为了提供一些符号和常量,便于指令的识别。

img

方法的调用

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

  • 静态链接
    当一个 字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
  • 动态链接
    如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。

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

  • 早期绑定
    早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。
  • 晚期绑定
    如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。

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

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

image-20201006162129496

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
package com.kylin.java2;

/**
* 说明早期绑定和晚期绑定的例子
*/
class Animal {

public void eat() {
System.out.println("动物进食");
}
}

interface Huntable {
void hunt();
}

class Dog extends Animal implements Huntable {
@Override
public void eat() {
System.out.println("狗吃骨头");
}

@Override
public void hunt() {
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("捕食耗子,天经地义");
}
}

public class AnimalTest {
public void showAnimal(Animal animal) {
animal.eat();//表现为:晚期绑定
}

public void showHunt(Huntable h) {
h.hunt();//表现为:晚期绑定
}
}

虚方法和非虚方法

子类对象的多态性使用前提:实际开发编写代码中用的接口,实际执行是导入的的三方jar包已经实现的功能

①类的继承关系(父类的声明)②方法的重写(子类的实现)

非虚方法

  • 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法
  • 静态方法、私有方法、final方法、实例构造器(实例已经确定,this()表示本类的构造器)、父类方法(super调用)都是非虚方法

其他所有体现多态特性的方法称为虚方法

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

普通调用指令:

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

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

  3. invokevirtual调用所有虚方法;

  4. invokeinterface:调用接口方法(虚方法);

动态调用指令(Java7新增)

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

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。

其中invokestatic指令和invokespecial指令调用的方法称为非虚方法

其中invokevirtual(final修饰的除外,JVM会把final方法调用也归为invokevirtual指令,但要注意final方法调用不是虚方法)、invokeinterface指令调用的方法称称为虚方法。

image-20201006163732130

image-20201006165647003

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

/**
* 解析调用中非虚方法、虚方法的测试
* <p>
* invokestatic指令和invokespecial指令调用的方法称为非虚方法
*
* @author shkstart
* @create 2020 下午 12:07
*/
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("atguigu.com");
//invokestatic
super.showStatic("good!");
//invokespecial
showPrivate("hello!");
//invokespecial
super.showCommon();

//invokevirtual
showFinal();//因为此方法声明有final,不能被子类重写,所以也认为此方法是非虚方法。
//虚方法如下:
//invokevirtual
showCommon();//有可能当前son类会重写该方法
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();
}

invokedynamic指令

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

动态类型语言和静态类型语言

  • 动态类型语言和静态类型语言两者的却别就在于对类型的检查是在编译期还是在运行期,满足前者就是静态类型语言,反之则是动态类型语言。
  • 直白来说 静态语言是判断变量自身的类型信息;动态类型语言是判断变量值的类型信息,变量没有类型信息,变量值才有类型信息,这是动态语言的一个重要特征
  • Java是静态类型语言(尽管lambda表达式为其增加了动态特性),js,python是动态类型语言.

image-20201006170501573

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
/**
* 体会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;
});
}
}

方法重写的本质

  • 找到操作数栈的第一个元素所执行的对象的实际类型,记作C。
  • 如果在类型C中找到与常量池中的描述符、简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束;如果不通过,则返回java.lang.IllegalAccessError异常。
  • 否则,按照继承关系从下往上依次对c的各个父类进行第二步的搜索和验证过程。
  • 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。 IllegalAccessError介绍 程序视图访问或修改一个属性或调用一个方法,这个属性或方法,你没有权限访问。一般的,这个会引起编译器异常。这个错误如果发生在运行时,就说明一个类发生了不兼容的改变。

虚方法表

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

举个例子:我们定义三个类、一个Friendly接口

image-20201006173250720

image-20201006173331538

Dog虚方法表中,toString()sayHello()方法都是指向的Dog类中的。其他的则指向它的父类Object

image-20201006173526202

CockerSpaniel的虚方法表中,sayHello()sayGoodbye()方法都是指向的类CockerSpaniel中的。而toString则指向父类Dog类,其他的指向Object。

image-20201006173845013

Cat的虚方法表中,eat(),sayHello(),sayGoodbye(),toString(),finalize()都指向Cat类,其他的指向父类Object

image-20201006192035172

方法返回地址-RA

  • 存放调用该方法的PC寄存器的值。
  • 一个方法的结束,有两种方式:
    • 正常执行完成
    • 出现未处理的异常,非正常退出
  • 无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者(方法的调用者可能也是一个方法)的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出时,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
  • 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
  • 正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

当一个方法开始执行后,只有两种方式可以退出这个方法

1.执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;

  • 一个方法在正常调用完成之后究竟需要使用哪一个返回指令还需要根据方法返回值的实际数据类型而定
  • 在字节码指令中,返回指令包含ireturn(当返回值是boolena、byte、char、short和int类型时使用)、lreturn、freturn、dreturn以及areturn(引用类型的)
  • 另外还有一个return指令供声明为void的方法、实例初始化方法、类和接口的初始化方法使用

image-20201006193447484

2.在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜素到匹配的异常处理器,就会导致方法退出,简称异常完成出口
方法执行过程中抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码。

image-20201006194002985

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
/**
*
* 返回指令包含ireturn(当返回值是boolean、byte、char、short和int类型时使用)、
* lreturn、freturn、dreturn以及areturn,另外还有一个return指令供声明为void的方法、
* 实例初始化方法、类和接口的初始化方法使用。
*/
public class ReturnAddressTest {
public boolean methodBoolean() {
return false;
}

public byte methodByte() {
return 0;
}

public short methodShort() {
return 0;
}

public char methodChar() {
return 'a';
}

public int methodInt() {
return 0;
}

public long methodLong() {
return 0L;
}

public float methodFloat() {
return 0.0f;
}

public double methodDouble() {
return 0.0;
}

public String methodString() {
return null;
}

public Date methodDate() {
return null;
}

public void methodVoid() {

}

static {
int i = 10;
}


//
public void method2() {

methodVoid();

try {
method1();
} catch (IOException e) {
e.printStackTrace();
}
}

public void method1() throws IOException {
FileReader fis = new FileReader("atguigu.txt");
char[] cBuffer = new char[1024];
int len;
while ((len = fis.read(cBuffer)) != -1) {
String str = new String(cBuffer, 0, len);
System.out.println(str);
}
fis.close();
}


}

一些附加信息

栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息。(很多资料都忽略了附加信息)