JAVA虚拟机(JVM)——虚拟机字节码执行引擎(一)

来源:互联网 发布:seo教程视频 编辑:程序博客网 时间:2024/06/03 19:59

概述

    执行引擎是JVM最核心的组成部分之一。“虚拟机”是一个相对于“物理机”的概念,这两种机器都有代码执行能力,其区别是物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,二虚拟机的执行引擎是由自己实现的,因此可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。    在不同的虚拟机实现里面,执行引擎在执行Java代码的时候可能会有解释执行和编译执行两种选择,也可能两者兼备,甚至还可能会包含几个不同级别的编译器执行引擎。但从外观上看起来,所有Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

运行时栈帧结构

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

这里写图片描述

局部变量表

    局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,建立在线程的堆栈上,是线程私有的数据,所以无论读写两个连续的Slot是否为原子操作,都不会引起数据安全问题。(明确规定需要两个Slot也就是64位的数据类型只有long和double,reference类型可能是32位也可能是64位)    在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程的,如果执行的是实例方法(非static方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中通过关键字this来访问。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。    为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域不一定是整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以给其他变量使用。不过某些情况下Slot的复用会直接影响到系统的垃圾收集行为。

Slot复用对GC的影响

//代码1public static void main(String[] args)(){    byte[] placeholder = new byte[64 * 1024 * 1024];    System.gc();}
运行代码1并查看系统垃圾回收过程,发现没有回收placeholder所占内存,因为在执行System.gc()时,变量placeholder还处于作用域内。
//代码2public static void main(String[] args)(){    {        byte[] placeholder = new byte[64 * 1024 * 1024];    }    System.gc();}
加了"{}"之后,placeholder的作用域被限制在"{}"内,从代码逻辑上讲,在执行System.gc()的时候,placeholder已经不可能再被访问了,但执行这段程序,运行结果还是有64MB的内存没有被回收。
//代码3public static void main(String[] args)(){    {        byte[] placeholder = new byte[64 * 1024 * 1024];    }    int a = 0;    System.gc();}
运行代码3之后,发现这次内存被正确回收了。通过这三段代码的运行结果,我们发现placeholder能否被回收的根本原因是:局部变量表中的Slot石否还存有关于placeholder数组对象的引用。所以在对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到JIT的编译条件等情况下,可以手动将变量赋值为null,来促使GC的执行。

操作数栈

     操作数栈(Operand Stack)也常称为操作栈,当一个方法刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取数据。     例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其他方法的时候是通过操作数栈来进行参数传递的。举个例子,证书假发字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这条指令时,会将这两个int值出栈并相加,然后将计算结果入栈。    在概念模型中,两个栈帧作为虚拟机的元素,是完全相互独立的。但是在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以共用一部分数据,无需进行额外的参数复制传递,重叠的过程如下图所示:

这里写图片描述

动态连接

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

方法返回地址

    在方法退出后,需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它上层方法的执行状态。一般来说,方法正常退出时。调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。   方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈的操作数栈中,调整PC计数器的值以指向方法调用指令后面的一条指令等。

附加信息

    虚拟机规范允许具体的虚拟机实现增加一些规范里没有描述的信息到栈帧之中,例如与调试相关的信息,这部分信息完全取决于具体的虚拟机实现。在实际开发中,一般会把动态连接、方法返回地址与其他附加信息全部归为一类,称为栈帧信息。
阅读全文
0 0
原创粉丝点击