第八章 虚拟机字节码执行引擎
字节码是如何被虚拟机执行从而完成指定功能呢?我来们了解一下jvm虚拟机底层的原理。
“虚拟机”是有别与物理机的概念,物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上。
而虚拟机的执行引擎是自已实现的,因此可以自行的制定指令集与执行引擎的结构,并且能够执行那些不被硬件直接支持的指令集格式。
了解两个概念,解释执行和编译执行。
解释执行是指通过解释器执行的模式
编译执行是指通过即时编译器产生本地代码执行的模式。
两种模式目前jvm都支持,还可以混合使用,具体什么时候解释执行,什么时候编译执行,这也看不同的jvm虚拟机实现,也有参数可调。
一、运行时栈帧结构
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的虚拟机栈的栈元素。
在活动线程中,只有栈顶的栈帧才是有效的,称为当前栈帧,执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。
1、 局部变量表
是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
局部变量的空间以变量槽Slot为最小单位。32位占一个Slot.
32位以内的数据类型:boolean、byte、char、short、int 、float、reference和returnAddress8种
64位数据类型:double、long
reference的用法:
- 直接或间接的对象在堆中的索引起始地址
- 对象所属数据类型在方法区中存储的类型信息
局部变量表是建立在线程的堆栈上,是线程私有的数据,随着线程的存在而存在,线程的消亡而消亡。
索引位置:
非static方法变量表是0的位置存的是 this,用于传递方法所属对象实例的引用。
static方法没有这个限制。
其它位置按顺序存储方法参数和局部变量
作用域:
{}是作用域,在{}中的局部变量过了{}花括号就失效,可以被回收。
2、操作数栈
操作数栈也称操作栈是一种后进先出的数据结构。
它有两个作用:
1)参数传递(通过入栈和出栈与本地变量表进行交互)
2)数学运算(支持jvm算术指令集)
jvm虚拟机的解释执行引擎称为“基于栈的执行引擎”。
3、动态链接
指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接。
静态解析:符号引用在类加载阶段或在第一次使用的时候就转化为直接引用。(前提是方法在程序真正运行之前就有一个可确定的调用版本,并且这个调用版本在运行期是不可改变的。也就是说编译时就能确定下来。)
动态连接:在每次运行期间转化为直接引用。(比如:多态的重写)
4、方法返回地址
只有两种方式可以退出这个方法:
- 当执行引擎遇到代表返回的指令
- 执行过程中遇到了异常,只要本地方法表的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。并且不会给它的上层调用者任何返回值。
二、静态分派和动态分派(找到方法)
https://blog.csdn.net/sunxianghuang/article/details/52280002
Human man=new Man();
我们把“Human”称为变量的静态类型,后面的“Man”称为变量的实际类型,
1、静态分派:
编译器在重载时是通过参数的静态类型而不是实际类型作为判定的依据。并且静态类型在编译期可知,因此,编译阶段,Javac编译器会根据参数的静态类型决定使用哪个重载版本。所有依赖静态类型来定位方法执行版本的分派动作称为静态分派
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,gentlemen!");
}
public void sayHello(Woman guy){
System.out.println("hello,lady!");
}
public static void main(String[] args) {
StaticDispatch sd = new StaticDispatch();
Human man=new Man();
Human woman=new Woman();
sd.sayHello(man);
sd.sayHello(woman);
sd.sayHello((Man)man);
}
}
hello,guy!
hello,guy!
hello,gentlemen!
2、动态分派:
我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。多见于重写
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();
}
}
输出:
man say hello!
woman say hello!
woman say hello!
我们从invokevirtual指令的多态查找过程开始说起,invokevirtual指令的运行时解析过程大致分为以下几个步骤:
1、找到操作数栈顶的第一个元素所指向的对象的实际类型,记作C。
2、如果在类型C中找到与常量中的描述符和简单名称相符合的方法,然后进行访问权限验证,如果验证通过则返回这个方法的直接引用,查找过程结束;如果验证不通过,则抛出java.lang.IllegalAccessError异常。
3、否则未找到,就按照继承关系从下往上依次对类型C的各个父类进行第2步的搜索和验证过程。
4、如果始终没有找到合适的方法,则跑出java.lang.AbstractMethodError异常。
由于invokevirtual指令执行的第一步就是在运行期确定接收者的实际类型,所以两次调用中的invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上,这个过程就是Java语言方法重写的本质。
我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派。
java语言是一门静态多分派,动态单分派的语言
三、基于栈的字节码解释执行引擎(执行方法)
这部分是讲虚拟机是如何执行方法中的字节码指令的。
1、编译过程
在java语言中,编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。这部分在java虚拟机之外进行的。
中的间是解释执行过程。
最下面的是编译执行的过程。
2、基于栈的指令集和基于寄存器的指令集
1) 基于栈的指令集,javac编译之后生成的二进制字节码文件就是基于栈的指令集架构。它是针对虚拟机的使用的,
优点:
可移植(不依赖于硬件和操作系统环境)
缺点:执行速度相对于寄存器架构慢一些。(频繁的出栈入栈,内存访问)
例子:1+1
iconst_1
iconst_1
iadd
istore_0
2)基于寄存器的指令集
基于寄存器工作,能够被cpu直接识别,
优点:执行速度快
缺点:可移植性差
例子:1+1
mov eax, 1
add eax,1
把eax寄存器的值设为1,然后add加1,最后保存在eax寄存器里。对比一下,它没有压栈和出栈的操作。
3、基于栈的解释器执行过程
昨天已经分析过了,不再细说。
int a = 2;
int b = 3;
int c = a + b;
System.out.println(c);
总结:
1、栈帧的结构,每部分的作用
2、虚拟机是如何找到正确的方法
3、虚拟机是如何执行方法
第九章是一些案例和实战,关于类加载器的模型和字节码生成技术。这章我们跳过,大家下来自己看。