JVM——虚拟机字节码执行引擎

目录

1、概述 

2、运行时栈帧结构

2.1局部变量表

2.2操作数栈

2.3动态连接

2.4方法返回地址

2.5附加信息

3、方法调用

3.1解析

3.2分派

3.3动态类型语言支持

4、基于栈的字节码解释执行引擎

4.1解释执行

4.2基于栈的指令集与基于寄存器的指令集

4.3基于栈的解释器执行过程



1、概述 

虚拟机的执行引擎是由自己实现的,因此可以自行制定指令集与执行引擎的结构关系,
并且能够执行那些不被硬件直接支持的指令集格式。

所有虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

2、运行时栈帧结构

  • 栈帧Stack Frame是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(virtual machine stack)的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始到执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。
  • 在编译程序代码时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的Code属性之中。因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现
  • 一个线程中的方法调用链可能会很长,很多方法都同时处于执行状态。对于执行引擎来说,在活动过程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧(current stack frame),与这个栈帧相关联的方法称为当前方法(current method)。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

2.1局部变量表

  • 局部变量表(local variable table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。
  • 在Java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。
  • 局部变量表的容量以变量槽(variable slot)为最小单位。一个slot可以存放一个32位以内的数据类型。
  • 虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0开始到局部变量表最大的slot数量。
  • 在方法执行时,虚拟机是使用局部变量表完成参数值参数变量列表的传递过程的。
  • 类变量有两次赋初值的过程:
    ①在准备阶段,赋予系统初始值;
    ②在初始化阶段,赋予程序员定义的初始值。
    因此,即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。
    
    但是局部变量就不一样了~~~~~
    如果一个布局变量定义了但没有赋初始值是不能使用的,
    不要认为Java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为FALSE等这样的默认值。

2.2操作数栈

  • 操作数栈(operand stack)也称为操作栈,它是一个后进先出栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks数据项中。
  • 操作数栈的每一个元素可以是任意的Java数据类型,包括long和double。32位数据类型所占的栈容量为1,64位数据类型所占的栈容量为2.
  • 当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容也就是入栈/出栈操作。
  • 举个栗子:整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。
  • 操作数栈中元素的数据类型必须与字节码指令的序列严格匹配。
  • 另外在概念模型中,两个栈帧作为虚拟机栈的元素,是完全相互独立的。
  • Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。

2.3动态连接

  • 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(dynamic linking)。
  • Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。
  • 这些符号引用一部分会在类加载阶段或第一次使用的时候就转化为直接引用,这种转化称为静态解析;
  • 另一部分在每一次运行期间转化为直接引用,这部分称为动态连接。

2.4方法返回地址

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

①执行引擎遇到一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法称为正常完成出口。(normal method invocation completion)

②在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,无论是Java虚拟机内部产生的异常,还是代码中使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,这种退出方法的方式称为异常完成出口。(abrupt method invocation completion)

一个方法使用异常完成出口的方式退出,是不会给他的上层调用者产生任何返回值的。

2、无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。

3、方法退出的过程实际上就等同于把当前帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

2.5附加信息

虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧中。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。

3、方法调用

方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内容的具体运行过程。

一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址。

3.1解析

  • 所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用,这种解析成立的前提是:方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。In other word,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(resolution)
  • 在Java虚拟机中提供了5条方法调用字节码指令:
invokestatic:调用静态方法
invokespecial:调用实力构造器<init>方法、私有方法和父类方法
invokevirtual:调用所有虚方法
invokeinterface:调用接口方法,会在运行时在确定一个实现此接口的对象
invokedynamic:先在运行时动态解析出调用点限定符所引用的方法,然后在执行该方法。
在此之前的4条调用指令,分派逻辑是固化在Java虚拟机内部的,而invokedynamic指令的分派逻辑是由用户所设定的引导方法决定的。
  • 只要能被invokestatic和invokespecial指定调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有:静态方法、私有方法、实例构造器、父类方法4种,他们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可称为非虚方法。除了final方法外,其他方法称为虚方法。
  • 虽然final方法是使用invokevirtual指令来调用的,但是由于他无法被覆盖,没有其他版本,在Java语言规范中明确说明了final方法是一种非虚方法。
解析调用一定是个静态的过程,在编译期间就完全确定,在类装载的解析阶段就会
把涉及的符号引用转变为可确定的直接引用,不会延迟到运行期再去完成。

3.2分派

3.2.1静态分派

package jvm;
/** 
 * @author wyl
 * @time 2018年7月21日下午5:16:01
 */
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,man");
	}
	
	public void sayHello(Woman guy){
		System.out.println("hello,woman");
	}
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Human man=new Man();
		Human woman=new Woman();
		StaticDispatch sd=new StaticDispatch();
		sd.sayHello(man);
		sd.sayHello(woman);
	}

}
运行结果:
hello,guy
hello,guy

 Human man=new Man();

  • Human称为变量的静态类型,或者叫做外观类型;
  • Man称为变量的实际类型。

静态类型和实际类型在程序中都可以发生一些变化,区别如下:

        //实际类型变化

        Human man=new Man();
        Human woman=new Woman();

       //静态类型变化
        sd.sayHello(man);
        sd.sayHello(woman);

  • 静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的静态类型是在编译期可知的
  • 实际类型变化的结果在运行期才可确定,编译器在编译程序的时候并不知道一个对象的实际类型是什么。

 虚拟机(确切地说是编译器)在重载时是通过参数的静态类型而不是实际类型作为判定依据的。静态类型在编译期是可知的。

所有依赖静态类型来定位执行版本的分派动作称为静态分派。静态分派的典型应用是方法重载。

3.2.2动态分配

它和重写override有着很密切的关联。

package jvm;

import jvm.StaticDispatch.Human;
import jvm.StaticDispatch.Man;
import jvm.StaticDispatch.Woman;

/** 
 * @author wyl
 * @time 2018年7月21日下午5:37:41
 */
public class DynamicDisptch {

	static abstract class Human{
		protected abstract void sayHello();
	}
	
	static class Man extends Human{
		@Override
		protected void sayHello() {
			System.out.println("man,hello");
		}
	}
	static class Woman extends Human{
		@Override
		protected void sayHello() {
			System.out.println("woman,hello");
		}
	}
	
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		Human man=new Man();
		Human woman=new Woman();
		man.sayHello();
		woman.sayHello();
		man=new Woman();
		man.sayHello();
	}

}
运行结果:
man,hello
woman,hello
woman,hello

 在运行期根据实际类型确定方法执行版本的分派过程称为动态分派

3.2.3单分派与多分派

3.3动态类型语言支持

4、基于栈的字节码解释执行引擎

许多虚拟机的执行引擎在执行Java代码的时候都有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。

4.1解释执行

4.2基于栈的指令集与基于寄存器的指令集

4.3基于栈的解释器执行过程

猜你喜欢

转载自blog.csdn.net/u014067137/article/details/81142859