java虚拟机随手笔记(7)虚拟机字节码执行引

来源:互联网 发布:订单生成软件 编辑:程序博客网 时间:2024/06/05 18:42

运行时栈帧结构

1栈帧的作用

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

2.栈帧的大小

在编译器的时候就已经确定了每个栈帧的打消了,并且都放入到class文件(字节码文件)的Code区域了。详见java虚拟机随手笔记(6)

3.栈帧的结构

一个线程中,方法的调用链可能很长,因此栈中可能存储着多个栈帧,但是只有处于栈顶部的栈帧才是有效的,成为当前栈帧(Current Stack Frame),如下图所示:


4.局部变量表

用于存放方法参数和方法内部定义的局部变量。在java程序编译为Class文件时,就在方法的Code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。局部变量表的容量已变量槽(Variable Slot 简称Slot),每个Slot规定能够存放一个boolean,byte,char,short,int,float,reference或returnAddress的数据类型(但没有规定其容量,可以视不同编译器和OS改变)。其中reference类型表示对一个对象实例的引用,必须做到:一是能够从此引用直接或间接的查找到这个对象在堆中的位置,二是能够通过此引用直接或间接的查找到对象所属数据类型在方法区中的存储的类型信息(即Class对象)。当然,64位数据,虚拟机会分配两个Slot空间给它,比如Long和Double

5.方法执行时局部变量表的回收

方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程,如果执行的是实例方法(非Static方法),那么局部变量表中的0位Slot(就是第一个slot)存储的是当前方法所属对象的引用,比如this关键字。这里的回收还是很有意思的,要理解回收的过程,我们先看几个例子,下面的例子都是一段代码跟着一段GC信息
代码:

GC信息:

代码:

GC信息
代码:
GC信息:

看以上的6张图片,我们看Full GC的过程,前两次都没有回收掉我们的空间,只有第三次才回收掉了空间。这是为什么呢?原因是局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。而上述三次GC过程,前两次没有被回收的原因是没有别的变量覆盖掉原先的slot,也就是说,这个slot可能还存在,所以没有被回收掉,虽然它的生命周期已经结束了。

placeholder能否被回收的根本原因是:局部变量表中的Slot是否还存有关于placeholder数组对象的引用。 第一次修改中,代码虽然已经离开了placeholder的作用域,但在此之后,没有任何对局部变量表的读写操作,placeholder原本所占用的Slot还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。 这种关联没有被及时打断,在绝大部分情况下影响都很轻微。 但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存、 实际上已经不会再使用的变量,手动将其设置为null值(用来代替那句int a=0,把变量对应的局部变量表Slot清空)便不见得是一个绝对无意义的操作,这种操作可以作为一种在极特殊情形(对象占用内存大、 此方法的栈帧长时间不能被回收、 方法调用次数达不到JIT的编译条件)下的奇技来使用。 

6、操作数栈

也常被成为操作栈,它是一个后入先出的栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写入到Code属性的max_stacks中了。在方法执行刚刚开始id时候,操作数栈是空的,随着执行会有一个一个的数据写入到操作数栈或者出栈。这里存的就是数据了,符号表存的是各种符号(int,double,reference等)。

7,动态连接

每个栈帧都包含一个指向运行时常量池中该栈帧所述方法的引用,持有这个引用是为了支持方法调用过程中的动态链接。常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。这些符号引用一部分会在类加载阶段或者第一次使用的时候就转化为直接引用,这就是静态链接。动态链接是在运行时需要这个变量或者引用是,将符号引用转化为直接引用的过程。

8.方法返回地址

方法退出的方式只有两种,一种是遇到方法返回字节码,称为正常完成出口(Normal Method Invocation Completion);还有一种退出方式是方法执行过程中遇到异常,并且这个异常没有在方法体内得以处理。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。 而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。 方法退出实际上就是栈帧初栈的工作。

9.附加信息:

在实际开发中,一般会把动态链接,方法返回地址与其他附加信息全部归为一类,成为栈帧信息。

方法调用

方法调用并不等同于方法执行,方法调用阶段唯一多任务就是确定调用的方法,暂时不涉及方法内部的具体过程。在程序运行时,进行方法调用是最普遍、最频繁的操作,但前面已经提到过,class文件的编译过程不包含传统编译过程中的链接步骤,一切方法调用在class文件里面存储的都只是符号引用。而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)。

1.解析

所有方法调用的目标方法在Class文件的常亮池中都是一个常量池的符号引用,部分方法会在类加载的解析阶段转化为直接引用,这种转化能成立的前提是:方法在程序运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的。这类方法的调用称为解析(Resolution)。
适合类加载阶段解析的方法有:静态方法和私有方法。这些方法不能被继承,因此可以解析。java虚拟机提供了5条方法调用指令,凡是能够被invokestatic和invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,因此静态方法,私有方法,实力构造器、父类方法都是静态链接更多的方法如下:



上述四种方法也叫作非虚方法,除此之外,final方法虽然使用invokevirtual指令来调用的,但是它无法被覆盖,因此也是一种非虚方法。

2.分派

解析一定是一个静态的过程,在编译期间就完全确定。但是分派不一定,可能是静态的也可能是动态的,根据分派的宗量数也可以分为但分派和多分派,因此两两结合就形成了静态单分派,动态单分派,静态多分派,动态多分派。

  • 静态分派

我们先看一个面试中经常遇到的例子


上述执行结果为什么将所有的Man和women都当做了Human来执行呢?我们先看这行代码:Human man = new Man();我们这句代码中,Human称为变量的静态类型(static type),或者叫做外观类型(Apparent Type),后面的Man则称为变量的实际类型(Actual Type),静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会改变,并且最终的静态类型是在编译期可知的,而实际类型变化的结果在运行期才可以确定。编译器在编译一个程序的时候,并不知道一个对象的实际类型是什么。

准确的说,虚拟机在重载时,使用的是参数的静态类型而不是动态类型,于是,就发生了上述情况。

除非你使用强制类型转换。如下所述

sr.sayHello((Man)man);
sr.sayHello((Woman)women);

虽然编译器能确定出方法的重载版本,但是在很多情况下,重载版本不是唯一的,比如各种类型转化,int,char,byte之间的转换。那么这个时候只能确定一种最好的版本,而不是随便一个版本。如下图


我们知道java的类型转换是往安全的方向转换的,也即是说,位数低的朝位数高的转化:


当不能转化时,如果有自己的封装器类存在,比如上述的代码,注释掉int和long之后,就会调用Character,也就是输出Say Character。再注释掉,就会hello Serializable。在注释,就会成为hello Object;再继续注释掉,就变成了hello char...可以看出,这种可变长参数优先级是最低的。


总结下,如果是基本类型,优先是自己的类型,其次是向上转型,再次是包装类,再次是包装类的父类,再次是包装类的接口(如果没有继承,父类和接口不能同时出现),再次是Object,再次是可变长参数。


  • 动态分派

静态分派是和重载同名方法相关的,而动态分派和子类重写父类方法相关的。动态分配很简单,不再多讲。


  • 单分派与多分派

方法的接受者与方法的参数统称为方法的总量。根据分派基于多少总量,可以将分派分为单分派和多分派两种。单分派是根据一个宗量对目标方法进行选择,多分派是根据多于一个宗量对目标方法进行选择。

今天的java语言是一种静态多分派,动态单分派的语言,我们看下面的例子:



在main函数中,调用了两次hardChoice()方法,这两次hardChoice()方法的编译过程和执行是这样的:在编译阶段,也就是静态分配阶段,这是选择目标方法的依据有两点,一是静态类型Father还是Son来调用hardChoice()方法,二hardChoice()方法的参数是QQ还是360。静态分配选择结果的最终产物是产生了两条invokevirtual指令,两条指令的参数分别为常量翅中指向Father.hardChoice(360)以及Father.hardChoice(QQ)方法的符号引用。因为是根据两个宗量选择,所有静态分派是多分派。
动态分派阶段,也及时执行阶段, 虚拟机选择哪一个类对象的hardChoice知识根据Son和Father来选择的,因此动态分派是单分派。


先写到这里吧。剩下的有空再搞

0 0
原创粉丝点击