此阶段时用于初始化类变量和其它资源,是执行类构造器()方法的过程,此时才真正开始执行勒种定义的java程序代码。
第八章:字节码执行引擎
===============
JVM中的执行引擎在执行java代码的时候,一般有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。
一、栈帧
1、基本概念
(1)、定义
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它位于虚拟机栈里面。
(2)、作用
每个方法从调用开始到执行完成的过程中,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
(3)、特点
-
栈帧包括了局部变量表,操作栈等,到底是需要多大的局部变量表,多深的操作栈是在编译期确定的。因为一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响。
-
两个栈帧之间的数据共享。在概念模型中,两个栈帧完全独立,但是在虚拟机的实现里会做一些优化处理,令两个栈帧出现一部分重叠。这样在进行方法调用时,就可以共用一部分数据,无须进行额外的参数复制传递。
2、局部变量表
局部变量表是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
//方法参数
max(int a,int b)
局部变量和类变量(用static修饰的变量)不同
//全局变量
int a;
//局部变量
void say(){
int b=0;
}
类变量有两次赋初始值的过程:准备阶段(赋予系统初始值)和初始化阶段(赋予程序定义的初始值)。所以即使初始化阶段没有为类变量赋值也没关系,它仍然有一个确定的初始值。
但局部变量不一样,如果定义了,但没赋初始值,是不能使用的。
3、操作栈
当一个方法刚刚开始执行的时候,这个方法的操作栈是空的,在方法的执行过程中,会有各种字节码指令往操作栈中写入和提取内容,也就是出栈、入栈操作。
例如,计算:
int a = 2+3;
当执行iadd指令时,会将2和3出栈并相加,然后将相加的结果5出栈。
4、动态链接
Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用分为两个部分:
(1)静态解析:在类加载阶段或第一次使用的时候就转化为直接引用。
(2)动态链接:在每一次运行期间转化为直接引用。
5、返回地址
当一个方法开始执行后,只有两种方式可以退出这个方法:正常退出、异常退出。无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行。
(1)正常退出:调用者的PC计数器作为返回地址,栈帧中一般会保存这个计数器值。
(2)异常退出:返回地址是通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。
二、方法调用
1、解析
对“编译器可知,运行期不可变”的方法进行调用称为解析。符合这种要求的方法主要包括:
(1)静态方法,用static修饰的方法
(2)私有方法,用private修饰的方法
2、分派
分派讲解了虚拟机如何确定正确的目标方法。分派分为静态分派和动态分派。讲解静动态分派之前,我们先看个多态的例子。
Human man=new Man();
在这段代码中,Human为静态类型,其在编译期是可知的。Man是实际类型,结果在运行期才可确定,编译期在编译程序的时候并不知道一个对象的实际类型时什么。
(1)静态分派
所有依赖静态类型来定位方法执行版本的分派动作称为静态分派。它的典型应用是重载。
public class StaticDispatch{
static abstract class Human{
}
static class Man extends Human{
}
static class Woman extends Human{
}
public void say(Human hum){
System.out.println("I am human");
}
public void say(Man hum){
System.out.println("I am man");
}
public void say(Woman hum){
System.out.println("I am woman");
}
public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
StaticDispatch sr = new StaticDispatch();
sr.say(man);
sr.say(woman);
}
}
运行结果是:
I am human
I am human
为什么会产生这个结果呢?
因为编译器在重载时,是通过参数的静态类型而不是实际类型作为判断依据的。在编译阶段,javac编译器会根据参数的静态类型决定使用哪个重载版本,所以两个对say()方法的调用实际为sr.say(Human)。
(2)动态分派
在运行期根据实际类型确定方法执行版本的分派过程,它的典型应用是重写。
public class DynamicDispatch{
static abstract class Human{
protected abstract void say();
}
static class Man extends Human{
@Override
protected abstract void say(){
System.out.println("I am man");
}
}
static class Woman extends Human{
@Override
protected abstract void say(){
System.out.println("I am woman ");
}
}
public static void main(String[] args){
Human man = new Man();
Human woman = new Woman();
man.say();
woman.say();
man=new Woman();
man.say();
}
}
I am man
I am woman
I am woman
这似乎才是我们平时敲的java代码。对于方法重写,在运行时才确定调用哪个方法。由于Human的实际类型时man,因此调用的是man的name方法。其余的同理。
动态分派的实际依赖于方法区中的虚方法表,它里面存放着各个方法的实际入口地址。如果某个方法在子类中被重写了,那子类方法表中的地址将会替换为指向子类实际版本的入口地址,否则,指向父类的实际入口。
(3)单分派和多分派
方法的接收者与放的参数统称为方法的宗量,分为单分派和多分派。
单分派是指根据一个宗量就可以知道调用目标(即应该调用哪个方法),多分派需要根据多个宗量才能确定调用目标。
在静态分派中,需要调用者的实际类型和方法参数的类型才能确定方法版本,所以其是多分派型。
在动态分派中,已经知道了参数的实际类型,所以此时只需知道方法调用者的实际类型就可以确定出方法版本,所以其是单分派类型。
综上,java是一门静态多分派,动态单分派的语言。
**三、基于栈的字节码解释执行引擎**
-------------------
### 1、解释执行

java语言中,javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一部分动作在java虚拟机以外进行,解释器在虚拟机的内部,所以java程序的编译是半独立的实现。
### 2、基于栈的指令集与基于寄存器的指令集
(1)基本概念
java编译器输出的指令流,基本上是一种基于栈的指令集架构,指令流的指定大部分都是领地址指令,他们依赖操作数栈进行工作。
与之相对应的另一套常用的指令集架构师基于寄存器的指令集,最典型的就是x86的二地址指令集,通俗一点,就是我们主流PC机中直接支持的指令集架构,这些指令依赖寄存器进行工作。
(2)优缺点
基于栈的指令集优点:
* 基于栈的指令集主要优点就是可移植性,寄存器由硬件直接提供,程序直接依赖硬件寄存器则不可避免的要受到硬件的约束。
例如,现在32位80\*86体系的处理器中提供了8个32位的寄存器,如果使用栈架构的指令集,用户程序不会直接使用这些寄存器,就可以由虚拟机实现来自行决定把一些访问最频繁的数据(程序计数器、栈顶缓存等)放到寄存器中以获取尽量好的性能,这样实现看起来更简单一些。
* 代码相对更加紧凑
字节码中每个字节对应一条指令,而多地址指令集中还需要存放参数。
* 编译器实现更加简单
不需要考虑空间分配的问题,所需空间都在栈上操作。
基于栈的指令集缺点:
栈架构指令集的主要缺点是执行速度相对来说会缓慢一些,所以主流物理机的指令集都是寄存器架构的。
虽然栈架构指令集的代码非常紧凑,但是完成相同功能所需要的指令数量一般会比寄存器架构多,因为出栈、入栈操作本身就产生了相当多的指令。更重要的是,栈实现在内存之中,频繁的栈访问也就意味着频繁的内存访问,相对于处理器来说,内存始终是执行速度的瓶颈。尽管虚拟机可以采取栈顶缓存的手段,把最常用的操作映射到寄存器中避免直接内存访问,但这也只能是优化措施而不是解决本质问题的方法。由于指令数量和内存访问的原因,所以导致了栈架构指令集的执行速度相对较慢。
### 3、基于栈的解释器执行过程
虚拟机中的字节码解释执行引擎是基于栈的。下面通过一段代码来仔细看一下其解释的执行过程。
public int calc(){
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
第一步:将100入栈。
第二步:将操作栈中的100出栈并存放到局部变量中,后面的200,300同理。
第三步:将局部变量表中的100复制到操作栈顶。
第四步:将局部变量表中的200复制到操作栈顶。
第五步:将100和200出栈,做整型加法,最后将结果300重新入栈。
# 最后
**需要的朋友[戳这里即可免费获取](https://gitee.com/vip204888/java-p7)这份大牛的学习笔记哦~**

以导致了栈架构指令集的执行速度相对较慢。
### 3、基于栈的解释器执行过程
虚拟机中的字节码解释执行引擎是基于栈的。下面通过一段代码来仔细看一下其解释的执行过程。
public int calc(){
int a = 100;
int b = 200;
int c = 300;
return (a + b) * c;
}
第一步:将100入栈。
第二步:将操作栈中的100出栈并存放到局部变量中,后面的200,300同理。
第三步:将局部变量表中的100复制到操作栈顶。
第四步:将局部变量表中的200复制到操作栈顶。
第五步:将100和200出栈,做整型加法,最后将结果300重新入栈。
# 最后
**需要的朋友[戳这里即可免费获取](https://gitee.com/vip204888/java-p7)这份大牛的学习笔记哦~**
[外链图片转存中...(img-ROj5zzmH-1628422264792)]
