文章目录
1 虚拟机栈 JVM Stack
1.1 虚拟机栈简介
由于跨平台的设计,Java的指令都是根据栈来设计的,不同平台的CPU架构不同,所以不能设计为基于寄存器架构,JVM中栈管运行,堆管存储
每个线程被创建时都会创建一个虚拟机栈,该虚拟机栈是线程私有的,生命周期与线程相同,其内部是一个一个的栈帧 Stack Frame,一个栈帧中就对应一个Java方法
上述代码主线程的虚拟机栈结构示例:
1.2 虚拟机栈的作用
虚拟机栈的栈顶,即最上面的方法就是 当前方法,每次的方法调用就是一个个栈帧的出入栈操作
虚拟机栈的作用:
主管Java程序的运行,它保存方法的局部变量(8大基本类型,对象地址)
和部分结果,并参与方法的调用和返回
虚拟机栈的访问速度仅次于程序计数器,同样虚拟机栈不存在垃圾回收问题,JVM直接对虚拟机栈的操作只有两个:
1,每个方法执行,创建对应栈帧伴随着入栈
2,执行完后,方法对应的栈帧出栈
1.3 栈帧 Stack Frame
每个线程都有自己的虚拟机栈,栈中的数据都是以栈帧的格式存在的,在这个线程上正在执行的每个方法都各自对应一个栈帧,栈帧是一个数据集,维系着方法执行过程中的各种数据信息
一个运行的线程拥有自己的虚拟机栈,一个时间点上,只会有一个“活动”的栈帧,即只有栈顶的栈帧是有效的,这个栈帧被称为当前栈帧 Current Frame,与当前栈帧对应的方法就是当前方法 Current Method,定义这个方法的类就是当前类 Current Class
主线程开始先执行主方法,创建主方法栈帧,再执行方法1,创建栈帧1入栈成为当前栈帧,其中方法1调用了方法2,此时创建了新的栈帧,并且新栈帧成为当前栈帧,紧接着方法2调用方法3,当方法3执行完后,栈帧3出栈此时栈帧2成为当前栈帧,依次出栈直到主方法栈帧出栈,主线程结束
执行引擎运行的所有字节码指令只针对当前栈帧,程序计数器中存储的就是当前栈帧的指令地址
方法的结束分为两种方式:
1,正常结束,以return为代表
2,方法执行中出现未捕获处理的异常,以抛出异常的方式结束
可以参考上述示例的class文件反编译结果:
public double method3();
descriptor: ()D
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #10 // String method3ʼִ
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #11 // String method3ִн
13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: ldc2_w #12 // double 20.0d
19: dstore_1
20: dload_1
21: dreturn //dreturn 就是return double
LineNumberTable:
line 23: 0
line 24: 8
line 25: 16
line 26: 20
LocalVariableTable:
Start Length Slot Name Signature
0 22 0 this Lcom/coisini/jvm/core/StackFrameTest;
20 2 1 j D
指令地址21对应的指令是 dreturn 就是返回double类型数据
类似的 return ireturn也是如此
虚拟机栈用来操作栈帧出入栈,栈帧是其对应的Java方法的数据集,栈帧由以下结构组成:
局部变量表(Local Variable Table)
操作数栈(Operand Stack)
动态链接(Dynamic Linking)
方法返回地址(Return Address)
一些附加信息
1.3.1 局部变量表 Local Variable Table
局部变量表(Local Variable Table)是栈帧的组成部分,栈帧从属于虚拟机栈,所以局部变量表是线程的私有数据,不存在数据安全问题,局部变量表所需的容量大小是在编译期确定下来的,不可更改
当栈帧作为当前栈帧时,执行引擎执行根据程序计数器中的指令地址
执行操作指令,更改当前栈帧中的局部变量表和操作数栈以执行当前Java方法
当栈帧出栈时,对应方法执行完毕后,该栈帧的局部变量表也会销毁
局部变量表是一个数组,局部变量表最基本的存储单元是 变量槽 Slot,主要用于存储方法参数和定义在方法内部的局部变量,这些数据包括基本数据类型,对象引用(堆中实例的地址),以及returnAddress类型
32位以内的类型只占用一个Slot(除了long和double外的所有类型),64位的类型(long和double)占用两个Slot
方法method2()的局部变量表:
this也是一个变量,可以在构造方法和普通方法中使用,但不能用于静态方法中,因为静态方法的局部变量表不会存储this变量
局部变量表中的变量是当前方法私有的,在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程,当方法调用结束后,随着方法栈帧的销毁,从属的局部变量表也会销毁
局部变量表也与垃圾回收相关,局部变量表中直接或间接引用的对象都不会被回收
1.3.2 操作数栈 Operand Stack
每一个独立的栈帧中,除了局部变量表还包含一个先进后出的操作数栈,操作数栈随着当前方法开始执行时而创建,其最大深度在编译期就已定义好,32位类型占用一个栈单位深度,64位类型占用两个栈单位深度
操作数栈,在方法执行中,根据字节码指令
往栈中写数据(入栈)或从栈中提取数据(出栈)
来完成复制,交换,求和等操作
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
Java虚拟机的解释引擎是基于栈的执行引擎,栈指的就是操作数栈,执行引擎解释执行字节码指令时,执行add,store,load等操作时,都会从局部变量表和常量池中取值进行出栈相加/相乘等操作,再存储到局部变量表中,从而执行完整个方法
来看一段代码理解栈帧中的局部变量表和操作数栈:
执行引擎执行字节码时,局部变量表和操作数栈的内容如下:
JVM中执行Java方法就是通过执行引擎执行对应方法的字节码指令,而执行一个方法需要对方法中的参数和变量进行运算并返回指定的结果,局部变量表就负责存储各个变量的值,而操作数栈负责对这些变量运算更新,以此来执行完整个方法
在操作数栈中,操作数是存储在内存中的,因此频繁地进行内存的读/写操作,会降低执行引擎的工作效率,为此HotSpot VM提供了栈顶缓存技术,即将栈顶数据全部缓存到物理CPU的寄存器中,以此降低了对内存的频繁读/写,提升了执行引擎的执行效率
1.3.3 动态链接 Dynamic Linking
在Java源文件被编译成字节码文件时,方法引用都作为符号引用保存到class文件的常量池,其作用就是为了提供一些符号和常量,便于指令的识别,当Java程序启动后,在方法区中生成运行时常量池,运行时常量池保存符号引用对应的直接引用
来看一个示例:
其中method1()调用了 虚方法 method2(),在指令中可以看到这条:
1 invokevirtual #3 <com/coisini/jvm/core/DynamicLinking.method2>
#3在常量池中对应的内容:
invokevirtual #3 实际上就是执行<com/coisini/jvm/core/DynamicLinking.method2>
#3是常量池中的符号引用,其对应的直接引用是com.coisini.jvm.core.DynamicLinking.method2()
当method1()需要调用method2()时,需要通过动态链接在运行时常量池找到method2()的直接引用
动态链接的作用就是从运行时常量池中找到被调用虚方法的直接引用,以便执行引擎来执行该虚方法
1.3.4 方法的调用
1.3.4.1 静态链接和动态链接
将被调用方法的符号引用转换为直接引用有两种方式,转换的时机不同:
静态链接
当一个字节码文件被装载进JVM后,如果被调用的目标方法在编译期可知
且运行期保持不变,这种情况下将被调用方法的符号引用转换为直接引用的过程称为静态链接
动态链接
如果被调用方法在编译期无法被确定下来,即只能在程序运行时
将被调用方法的符号引用转换为直接引用的过程称为动态链接
1.3.4.2 早期绑定和晚期绑定
在JVM中,将被调用方法的符号引用转换为直接引用的时机与方法的绑定机制有关,对应的绑定机制分为早期绑定和晚期绑定
绑定是一个字段,方法,或者类的符号引用被替换为直接引用的过程,该过程只会发生一次
早期绑定
指被调用的目标方法在编译期可知,且运行期间保持不变,
即可以将该方法与所属的类进行绑定,这样一来,由于确定了被调用的目标方法是哪个
因此也就可以使用静态链接的方式将符号引用转换为直接引用
晚期绑定
如果被调用的方法在编译期无法被确定下来,只能根据程序运行时
根据实际的类绑定相关的方法,进行动态链接,这种方式称为晚期绑定
来看下面的示例:
class Animal {
public void show() {
System.out.println("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 {
// 这里是早期绑定 静态链接 因Cat方法的无参构造方法是确定
public Cat() {
super();
}
// 这里是早期绑定 静态链接 因Cat方法的单参String构造方法是确定
public Cat(String name) {
super();
}
@Override
public void eat() {
System.out.println("猫吃鱼");
}
@Override
public void hunt() {
System.out.println("猫抓老鼠");
}
}
public class AnimalTest {
public void animalEat(Animal animal) {
// 这里是晚期绑定 动态链接
// eat方法存在多个 并不确定 需要根据执行时具体的Animal对象来调用
animal.eat();
}
public void animalHunt(Huntable huntable) {
// 这里也是晚期绑定 动态链接
// hunt是接口中的方法 编译时无法确定使用哪个实现类的实现方法
huntable.hunt();
}
public void show(Animal animal) {
animal.show();
}
}
1.3.4.3 非虚方法和虚方法
如果方法在编译期就确定了具体的调用版本,该版本在运行时也不可变(不能多态执行),这样的方法称为非虚方法,非虚方法进行早期绑定,静态链接
能多态执行的方法,编译时不能确定版本的称为虚方法
虚方法进行晚期绑定,动态链接
非虚方法
静态方法,final方法,构造方法,私有方法
即不能多态执行的方法(不能重写的方法) 编译期间可以确定的方法
虚方法
除了非虚方法外的所有方法
可以多态执行的方法(可能被重写的方法) 编译期间不能确定的方法
1.3.4.4 JVM中方法的调用指令
JVM中提供了几种方法的调用指令:
普通调用指令
1,invokestatic 调用静态方法 (非虚方法)
2,invokespecial 调用<init>构造方法 (非虚方法)
3,invokevirtual 调用虚方法 (虚方法)
(final方法被调用时也是invokevirtual 是特例 final方法本质还是非虚方法)
4,invokeinterface 调用接口方法 (虚方法)
这4条指令在JVM中被固化,方法的调用不可人为干涉
动态调用执行
5,invokedynamic 动态解析出需要调用的方法,然后执行 (虚方法)
该指令支持用户确定方法版本
1.3.4.5 关于invokedynamic指令
invokedynamic指令是Java7才增加的,是为让Java实现“动态类型语言”支持而做的一种改进,但Java7并没有提供直接生成invokedynamic指令的方法,需要借助ASM这种底层字节码工具才能生成invokedynamic指令,直到Java8的Lambda表达式的出现,才可以直接生成invokedynamic指令
静态类型语言和动态类型语言的区别就在于对类型的检查时机
静态类型语言在编译期检查类型,动态类型语言在执行期检查类型
Java: String str = "130.5"; 编译时检查变量str的类型是否正确
Python: str = 130.5; 运行时明确str的类型为double
静态类型语言判断变量自身的类型信息
动态类型语言判断变量值的类型信息,变量没有类型信息,变量值才有类型信息
invokedynamic指令能让JVM支持动态语言,让JS,Python等语言也能工作在JVM上
1.3.5 方法返回地址 Return Address
方法返回地址用来存放调用该方法的程序计数器的当前值,以便被调用方法结束后,返回到它被调用的位置
一个方法的结束,有两种方式:
1,正常执行完成退出
2,出现未处理的异常,异常退出
无论是哪种退出方式,在退出后,需要回到该方法被调用的位置,方法正常退出时,调用者的程序计数器的值作为方法返回地址,程序计数器的值就是调用者当前执行指令的位置,也就是执行被调用的方法的位置,而通过异常退出的,返回地址要通过异常表来确定,栈帧中一般不会保存这部分信息
看这样一个示例:
该类的三个方法:
main:
0 new #3 <com/coisini/jvm/core/ReturnAddress>
3 dup
4 invokespecial #4 <com/coisini/jvm/core/ReturnAddress.<init>>
7 astore_1
8 aload_1
9 invokevirtual #5 <com/coisini/jvm/core/ReturnAddress.method1>
12 return
method1:
0 aload_0
1 invokevirtual #2 <com/coisini/jvm/core/ReturnAddress.method2>
4 return
method2:
0 return
运行顺序是main->method1->method2->method1->method1
回顾程序计数器,程序计数器用来记录当前方法正在执行的指令地址,是线程私有的,当主方法开始执行时
1,向虚拟机栈中开始入main栈帧,执行到9时调用了method1,method1栈帧入栈
method1栈帧保存了main栈帧调用它的指令位置9,作为方法返回地址
2,method1执行到1时调用了method2,method2栈帧入栈
method2保存了method1栈帧调用它的指令位置1,作为方法返回地址
3,method2执行完毕,程序计数器为method1服务
根据method2的方法返回地址,执行引擎直接从1继续执行method1
4,method1执行完毕后,程序计数器为main服务
根据method1的方法返回地址,执行引擎从9继续执行main,main执行完毕结束
1.3.5.1 正常返回时的指令
JVM中的返回指令:
return 返回值为void或构造方法的返回值
ireturn 返回值是boolean byte char short int
lreturn 返回值是long
freturn 返回值是float
dreturn 返回值是double
areturn 返回值是引用类型
本质上,方法的退出就是当前栈帧出栈的过程,此时需要恢复调用者的局部变量表,操作数栈,将返回值压入调用者栈帧的操作数栈,设置程序计数器的值,让调用者继续执行下去
1.3.5.2 异常表
方法执行过程中存储一个异常表,方便在发生异常时找到处理异常的代码,如果异常表不能处理该异常,方法就会异常退出
异常表:
方法执行时如果出现了异常,会先在异常表中查询该异常是否存在,存在则执行catch块中的内容,不存在则直接异常退出,整个线程停止
1.3.6 一些附加信息
栈帧中还运行携带与JVM实现相关的一些附加信息,如对程序调试提供支持的信息
附加信息不是必须的,不同的JVM对JVM规范有不同的实现,有些可能有附加信息存储在栈帧中,有些可能根本没有附加信息