深入理解JVM总结——虚拟机字节码执行引擎

来源:互联网 发布:定时语音播报软件 编辑:程序博客网 时间:2024/06/08 08:38

执行引擎是Java虚拟机最核心的组成部分之一。物理机和虚拟机都有执行代码的能力。区别在于物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行指定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。
虚拟机执行引擎在执行Java代码的时候分为解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行),也可以两者兼备
所有的Java虚拟机的执行引擎都是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。

运行时栈帧结构

栈帧,用于支持虚拟机进行方法调用和方法执行的数据结构,是虚拟机运行时数据区中的虚拟机栈的栈元素。存储了方法的局部变量、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用到执行完成,都对应着一个栈帧在虚拟机栈中从入栈到出栈的过程
栈帧的结构
编译代码时,栈帧的内存分配已经确定了,不会受到程序运行期变量数据的影响,而仅仅决定于具体的虚拟机实现
对于执行引擎来说,在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与之对应的方法称为当前方法。执行引擎运行的所有字节码指令都只针对当前栈帧进行操作

局部变量表

是一组变量值存储空间,用于存储方法参数和方法内部定义的局部变量。编译成Class文件时,在方法的Code属性的max_locals确定其最大容量。
局部变量表的容量已变量槽(Slot)为最小单位。每一个slot都能存放基本数据类型的数据,一般来说就是32位以内。64位虚拟机则以高位对齐分配两个连续slot空间(double,long)。对于64位,不允许单独只访问其中的一个,不然会在校验阶段抛异常。
方法执行时,虚拟机使用局部变量表完成参数值到参数列表的传递过程。为尽可能节省栈帧空间,局部变量表中的slot是可以重用的。方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那么这个变量对应的slot就可以交给其他变量使用。slot的复用在某些情况下会直接影响到系统的垃圾收集行为
局部变量表中的slot还存在关于数组等对象的引用,即使离开了代码的作用域,在此之后若没有任何堆局部变量表的读写操作的话,该引用原本所占用的slot还没有被其他变量所复用,所以作为GCRoots一部分的局部变量仍保持着对它的关联。

public statci void main(String[] args){    byte[] a=new byte[64*1024*1024];//作用域在gc之后    /////或者如下,也是没有将内存收回来    {            byte[] a=new byte[64*1024*1024];//出了括号就出了作用域        }    //添加如下代码,就变动了局部变量表,内存被真正回收。    int a=0;    System.gc();}

不使用的对象,占用大量内存,可以手动赋值为null,作用同int a=0那句。
类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;一次在初始化阶段,赋予程序猿定义的初始值。因此,即时在初始化阶段程序员没有为类变量赋值也无所谓,它仍具有一个确定的初始值。
局部变量不行,并不是所有都如int ,boolean一样都有默认初始值,不赋初始值局部变量是不能使用的。就算编译通过或手写字节码生成class文件,字节码校验的时候也会被虚拟机发现而导致类加载失败。

操作数栈

也作操作栈,后入先出栈。最大深度在编译时就写入Code属性的max_stacks中。操作数栈中每个元素可以是任意Java数据类型,包括long和double。32位数据类型占容量为1,64位的占2。在方法执行的任何时候,操作数栈的深度都不会超过max_stacks设定的最大值
操作数栈中元素数据类型必须与字节码指令的序列严格匹配,如i指令为iadd,则栈顶两个都为int的,不能出现long 和float。
在概念模型上,两个栈帧作为虚拟机的元素是相互独立的。但在大多数虚拟机实现里都优化处理,两个栈帧会出现一部分重叠。
两栈帧之间的数据共享
Java虚拟机的解释执行引擎又称为“基于栈的执行引擎”的栈就是操作数栈

动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法的调用过程中的动态连接
符号引用,一部分在类加载阶段或第一次使用时就转化为直接引用,即静态解析。而另一部分在运行期间转化为直接引用,这部分就为动态连接

方法返回地址

一个方法运行时,只有两种方式退出方法:一种是执行引擎遇到任意一个方法返回的字节码指令,可能会有返回值传递给上层的方法调用者,是否有返回值和返回值类型将根据遇到何种方法返回指令来决定。这种退出方式称为正常完成出口
第二种方式,在方法执行过程中遇到了异常,且异常没有在方法体内得到处理,无论Java虚拟机内部产生的异常还是代码使用athrow字节码指令产生的异常,只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出这种退出方式成为异常完成出口。它是不会给上层调用者返回任何值的。
方法退出等同于把当前栈出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数

附加信息

即栈帧信息。动态连接、方法返回地址、其他信息等。

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一任务就是确定被调用方法的版本(即调用的哪一个方法),暂时不涉及方法内部具体的运行过程。一切方法调用在class文件中存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(直接引用)。给Java带来强大动态扩展能力,也使其变得相对复杂,需要在类加载或是运行期间才能确定目标方法的调用。

解析

符号引用转化直接引用的解析阶段的前提:方法在程序真正运行之前就有一个可确定的调用版本,且这个方法的调用版本在运行期间是不可变的。也就是说调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用就称为解析
符合这类的有:静态方法和私有方法两大类。因为前者和类型直接关联,而后者在外部不可访问,由此决定了它们都不可能通过继承或别的方式重写其他版本,因此都适合在类加载阶段进行解析。
Java虚拟机,5条调用字节码指令,
invokestatic,静态方法
invokespecial(调用实例构造器方法、私有方法和父方法),
invokevirtual,虚方法(除上述两种和final以外的其他方法)
invokeinterface,接口方法,会在运行时再确定一个实现此接口的对象。
invokedynamic(先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法)。
被final修饰的方法,虽然是使用的invokevirtual指令来调用,但由于无法覆盖,没有其他版本,也无需进行多态选择,或其选择结构唯一,因此final也是一种非虚方法。
解析调用时一个静态过程,在编译期间就已确定,在类装在的解析阶段就会把涉及的符号引用全部阻焊变为可确定的直接引用,不会延迟到运行期再去完成。

分派

分派调用则可能是静态的(编译期完成)也可能是动态的(运行期完成),根据分派依据的宗量数(方法的调用者和方法的参数统称为方法的宗量)又可分为单分派和多分派。两类分派方式两两组合便构成了静态单分派、静态多分派、动态单分派、动态多分派四种分派情况。。

静态分派

未完待续,P247。


阅读全文
0 0
原创粉丝点击