JVM---10分钟入门代码执行过程(字节码执行引擎剖析)

1、物理机与虚拟机的区别

虚拟机是一个相对于物理机的概念。两种机器都有代码执行能力,区别是物理机的执行引擎是直接建立在处理器,硬件、指令集和操作系统层面上的,而虚拟机的执行引擎是自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

2、运行时栈帧结构

运行的每一个方法调用开始到执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素,栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。

在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定,并且写入到方法表的code属性,因此栈帧分配的内存大小,取决于具体的虚拟机实现。

对于执行引擎,无论这个线程方法调用链多长或者多少方法处于执行状态,执行引擎只处理栈顶的栈帧。—》当前栈帧,与相关联的方法称为当前方法。所以执行引擎运行的所有字节码指令都只针对当前栈帧操作。概念模型可以如下理解:
在这里插入图片描述

2.1局部变量表

如上面所说如果不了解可以点击这里对于类文件结构有个认识
在这里插入图片描述
红色框内的内容就是局部变量表是一组变量值存储空间,如下类文件示例:想要了解所有类文件结构可以点击这里
在这里插入图片描述
该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。可以使用 -g:none 或 -g:vars来取消或生成这项信息,如果没有生成这项信息,那么当别人引用这个方法时,将无法获取到参数名称,取而代之的是arg0, arg1这样的占位符。
在这里需要引入的另外一个LocalVariableTypeTable。仅仅是把记录的字段描述符的descriptior_index替换成了字段的特征签名
(Signature)由于描述附中泛型的参数化类型被擦除掉,描述符就不能准确描述泛型类型了,所以才需要这个变量属性LocalVariableTypeTable。对于非泛型类型来说,描述符合特征签名描述的信息基本一致。

Slot以变量槽作为最小单位,一个Slot能存放一个32位以内的数据类型,在虚拟机规范中没有说明Slot应占用的内存空间大小,只是很有导向性地说到每个Slot都能存放的数据类型有如下:

boolean
byte
char
short
int 
float
reference
returnAddress

在深入理解java虚拟机中,作者有个示例论证,是说在不使用的对象应手动赋值为null。这样操作可以作为一种极其特殊场景下(对象占用内存大,此方法的栈帧长时间不能被回收,方法调用次数达不到JIT的编译条件)。那么从作者的观点是说经过JIT编译器后,才是虚拟机执行代码的主要方式,赋null值的操作在经过JIT编译优化后就会被消除掉,这时候变量设置为null也没有意义。

在类变量结构中我们了解类文件加载会有两个地方赋初始值。一个是准备阶段,一个是赋值阶段。如果属于类的比如static。一个是初始化阶段。那么属于类的我们在准备的时候会给一个默认值,但是对于局部变量的时候,我们定义之后没有赋初始值则是不能使用的,会编译提示,如果通过手动生成字节码,那么字节码校验也会发现报错提示。
在这里插入图片描述

2.2操作数栈

后入先出,32位数据类型栈容量1,64位栈容量2.在方法执行的时候,操作数栈的深度不会超过max_stacks数据项中设定的最大值。
在概念模型中虽然两个栈帧作为虚拟机栈的元素是完全相互独立,但是在具体实现会有优化,达到两个栈帧数据共享节约空间。Java虚拟机的解释执行引擎称为:基于栈的执行引擎,其中栈—就是操作数栈。
在这里插入图片描述

2.3动态连接

Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。
静态解析—》这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用。
动态连接—》另一部分每次运行期间转化为直接引用

2.4方法返回地址

正常完成出口–比如return返回值,调用者的PC计数器值可以作为返回地址,栈帧中可能会保存这个计数器值。
异常完成出口–比如ithrow指令。throw异常。返回地址是通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

3、方法调用

方法调用阶段唯一的任务就是确定被调用方法的版本。Class文件的编译过程不包含传统编译中的链接步骤,一切方法调用在Class文件里面存储的都只是符号引用。而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)。所以什么时候可以得知确定目标方法的直接引用,是在类加载期间。

在这里插入图片描述
3.1解析
所有方法调用中的目标方法在Class文件里面都是一个常量池中的符号引用,在类加载的解析阶段,会将其中的一部分符号引用转化为直接引用。

编译期可知,运行期不可变。
5个字节码指令来帮助虚拟机达到这个要求
在这里插入图片描述
1和2指令会把符号引用解析为直接引用,这些方法可以称为非虚方法,与之相反称为非虚方法(除去final方法)

3.2分派
特点:静态、动态、单分派、多分派
组合:静态单分派、静态多分派、动态单分派、动态多分派
面向对象的3个基本特征:继承,封装,多态
静态分派—典型应用方法重载
在这里插入图片描述
假设入参是字符a的话执行顺序依次是,如果不信的话可以执行下代码,按照下面的顺序依次注释掉

import java.io.Serializable;

public class HelloWorld {

    public static void sayHello(Object arg) {
        System.out.println("hello Object");
    }

    public static void sayHello(int arg) {
        System.out.println("hello int");
    }

    public static void sayHello(long arg) {
        System.out.println("hello long");
    }

    public static void sayHello(Character arg) {
        System.out.println("hello Character");
    }

    public static void sayHello(char arg) {
        System.out.println("hello char");
    }

    public static void sayHello(char... arg) {
        System.out.println("hello char ...");
    }

    public static void sayHello(Serializable arg) {
        System.out.println("hello Serializable");
    }

    public static void main(String[] args) {
        sayHello('a');
    }
}

在这里插入图片描述
动态分派与多态性有关—重写

package org.fenixsoft.polymorphic;

/**
 * 方法动态分派演示
 * @author zzm
 */
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();
	}
}
/**
 * 单分派、多分派演示
 */
public class Dispatch {

	static class QQ {}

	static class _360 {}

	public static class Father {
		public void hardChoice(QQ arg) {
			System.out.println("father choose qq");
		}

		public void hardChoice(_360 arg) {
			System.out.println("father choose 360");
		}
	}

	public static class Son extends Father {
		public void hardChoice(QQ arg) {
			System.out.println("son choose qq");
		}

		public void hardChoice(_360 arg) {
			System.out.println("son choose 360");
		}
	}

	public static void main(String[] args) {
		Father father = new Father();
		Father son = new Son();
		father.hardChoice(new _360());
		son.hardChoice(new QQ());
	}
}

3.3动态类语言支持
在编译期进行类型检查过程的语言(C++和Java等)就是最常用的静态类型语言

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

4.1解释执行(编译过程)
可以点击这篇文章第二部分了解下:点击这里
在这里插入图片描述
在这里插入图片描述
Javac编译期完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程。因为这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以java程序的编译就是半独立的实现。
4.2基于栈的指令集与基于寄存器的指令集
1+1计算结果,基于栈的指令集如下
在这里插入图片描述
两条iconst_1指令连续把两个常量1压入栈后,iadd指令把栈顶的两个值出栈,相加,然后把结果放回栈顶,最后istore把栈顶的值放到局部变量表的第0个SLot中


基于寄存器的指令集如下
在这里插入图片描述
mov指令把EAX寄存器的值设为1,然后add指令再把这个值加1,结果就保存在EAX寄存器里面。

基于栈的指令集主要的优点是可移植,寄存器是由硬件直接提供。
栈架构指令集的主要缺点是执行速度会慢一些,受限于内存,目前市场的主流物理机的指令集都是寄存器架构。

4.3基于栈的解释器执行过程
在这里插入图片描述

如果不会得到如下图的执行代码,参考这篇文章
在这里插入图片描述
如下是图解执行过程
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

而上面的模型只是模拟了下简单的概念。实际应用中会有即时编译的多种优化手段。