第八章 虚拟机字节码执行引擎
执行引擎的输入是字节码的二进制流,输出是执行结果。
8.1 运行时栈帧结构
栈帧,方法执行时存储必要信息的数据结构。存在于虚拟机栈,每一个栈帧包含了局部变量表、操作数栈、动态连接、方法返回地址和附加信息。
8.1.1 局部变量表
存放方法参数和方法内部定义的局部变量。具体大小在class文件,方法表对应属性表的code属性中定义(max_local)。局部变量表中第0位索引的变量槽默认是方法所属的对象实例的引用。为了减少栈帧的内存消耗,变量槽是可以复用的。这里书中提到一个tips
public static void main(String[] args) { { byte[] nums = new byte[64 * 1024 * 1024]; } System.gc(); }
如上代码执行后,占用的64M内存并不会被回收,尽管执行回收操作时,已经超出了nums的作用域,但是局部变量表中的变量槽依然有nums数组对象的引用,所以不会回收。
public static void main(String[] args) { { byte[] nums = new byte[64 * 1024 * 1024]; } int a = 0; System.gc(); }
变量a的赋值操作,将nums原先占用的变量槽复用了,内存顺利回收。
在实际情况中,即时编译才是虚拟机执行代码的主要方式,第一份代码经过即时编译后,垃圾回收可以正确执行。
8.1.2 操作数栈
先进后出。在方法执行过程中,会有各种字节码指令往操作数栈中写入和提取内容。在概念模型中,两个栈帧是独立的,但是虚拟机会做一些优化:让下面栈帧中的部分操作数栈与上面栈帧的部分局部变量表重叠以节约空间和额外的复制传递。
8.1.3 动态连接
每个栈帧包含了一个指向运行时常量池中所属方法的引用。
8.1.4 方法返回地址
记录方法完成后,返回到最初方法被调用的地方。
8.2 方法调用
方法调用阶段,确定被调用方法的版本,暂未涉及到方法内部的具体运行过程。方法调用分为两种:解析和分派。
8.2.1 解析
在类加载的解析阶段,会将一部分符号引用转化成直接引用。但仅仅针对能够确定方法的版本,并且在运行期是不可改变的。符合这两个要求的有静态方法、私有方法、实例构造器方法、父类方法以及被final修饰的方法。
8.2.2 分派
静态类型和实际类型
Human human = new Man();
静态类型是Human,实际类型是Man。静态类型在编译期可知,而实际类型在运行期可知。
1. 静态分派
所有依赖静态类型确定调用方法版本的分派称为静态分派。方法重载
2. 动态分派
所有依赖实际类型确定方法调用版本的分派称为动态分派。子类重写父类方法
3. 单分派和多分派
方法的接收者(将要执行方法的所有者)和方法的参数统称为方法的宗量。根据分派基于多少宗量,分为单分派和多分派。