JVM十一:虚拟机字节码执行引擎(1)

         执行引擎是Java最核心的组成部分之一。虚拟机与物理机的区别:虚拟机是一个相对“物理机”的概念,这两种机器都有代码执行能力,其区别就是物理机的执行引擎是直接建立在处理器,硬件,指令集和操作系统层面上的,而虚拟机的执行引擎则是由自己实现的,因此可以自行指定指令集和执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式。

运行时栈帧结构

     栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息。

每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和额外信息。在编译程序代码的时候,栈帧需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到方法表的code属性中,因此一个栈帧需要分配多少内存,不会受但程序运行时期数据的影响,而仅仅取决于具体的虚拟机实现。那我们来具体了解一下栈帧结构。。。。。

一:局部变量表

   局部变量表是一组变量值存储空间,用于存放方法参数和方法内方布定义的局部变量。在Java程序编译为class文件时,就在方法的code属性的max_locals数据项中确定了该方法所需要分配的局部变量表的最大容量。

   在方法执行时,虚拟机是使用局部变量表完成参数变量列表的传递过程,如果是实例方法,那么局部变量表中的每0位索引的Slotslot是一个变量(应用或者实际类型的地址))默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字“this”来访问这个隐含的参数,其余参数则按照参数列表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域来分配其余的Slot。局部变量表中的Slot是可重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法,如果当前字节码PC计算器的值已经超出了某个变量的作用域,那么这个变量对应的Slot就可以交给其它变量使用。

局部变量与类变量的区别:  

      局部变量不像前面介绍的类变量那样存在“准备阶段”。类变量有两次赋初始值的过程,一次在准备阶段,赋予系统初始值;另外一次在初始化阶段,赋予程序员定义的值。

      因此即使在初始化阶段程序员没有为类变量赋值也没有关系,类变量仍然具有一个确定的初始值。但局部变量就不一样了,如果一个局部变量定义了但没有赋初始值是不能使用的。

局部变量表中得slot是可重用的,方法体定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超过了某个变量的作用域,那么这个变量对应的Slot就可以交给其他变量使用。这样的设计不仅仅是为了节省栈空间,在某些情况下Slot的复用会直接影响到系统的垃圾收集行为。例如如下代码:

/**

* VM args: -verbose:gc

*

*/

public class GCTest {

    public static void main(String[] args) {

    byte[] _64M = new byte[1024 * 1024 * 64];

    System.gc();

    }

}

运行结果:

[GC 66558K->65952K(129024K), 0.0015650 secs]

[Full GC 65952K->65853K(129024K), 0.0122710 secs]

从运行结果分析,发现System.gc()运行后并没有回收掉这64M的内存。

没有回收掉"_64M"的内存能说的过去,因为在执行System.gc()时,变量_64M还处于作用域之内,虚拟机自然不敢回收掉该内存。我们把代码位如下:

/**

* VM args: -verbose:gc

*

*/

public class GCTest {

    public static void main(String[] args) {

    {

    byte[] _64M = new byte[1024 * 1024 * 64];

    }

    System.gc();

    }

}
从代码逻辑上将,在执行System.gc()的时候,变量“_64M”已经不可能在被访问了,但执行以下这段程序,会发现运行结果如下

[GC 66558K->65968K(129024K), 0.0014760 secs]

[Full GC 65968K->65853K(129024K), 0.0127180 secs]

这是为什么呢?

在解释为什么之前,我们先对代码进行第二次修改。在调用 System.gc()之前加入代码int x=0,  这个修改看起来莫名其妙,但运行以下程序,却方法这次内存针对被正确回收了。

/**

* VM args: -verbose:gc

*

*/

public class GCTest {
    
    public static void main(String[] args) {

        {

        byte[] _64M = new byte[1024 * 1024 * 64];
        
        }

        int x=0;

        System.gc();

    }

}

[GC 66558K->65936K(129024K), 0.0027120 secs]

[Full GC 65936K->317K(129024K), 0.0129600 secs]

局部变量"_64M"能否被回收的根本原因就是:局部变量表中得Slot是否还存有关于_64M数组对象的引用。第一次修改,代码虽然离开了_64的作用域,但在此之后,没有任何对局部变量表的读写操作,_64M 原本所占用的Slot还没有被其他变量所复用,所以作为GC Roots 一部分的局部变量表让然保持对它的关联。这种关联没有被及时打断,在绝大部分情况下都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又占用了大量的内存,实际上已经不会在被使用的变量,手工将其设置为NULL值(用来代替int x=0)把变量对应的局部变量表Slot情况,就不是一个毫无意义的操作,这种操作可以作为 一种在及特殊情形(对象暂用内存大,此方法的栈帧长时间不能被回收,方法调用次数达不到JIT编译条件)下得“奇技” 来使用。但不应当对赋null值操作有过多的依赖,也没有必要把它当做一个普遍的编码方法来推广,以恰当的变量作用域来控制变量回收时间才是最优雅的解决方法。

      另外,赋null值的操作在经过虚拟机JIT编译器优化之后会被消除掉,这时候将变量设置为null实际上是没有意义的。字节码被编译为bending代码后,对GC Roots的枚举也与解释执行时期有所差别,在经过JIT编译后,System.gc()执行时就可以正确的回收掉内存。

二:操作数栈

   操作数栈是一个后入先出栈。同局部变量一样,操作数栈的最大深度也在编译的时候写入code属性的max_stacks数据项中。操作数栈的每一个元素可以是任意的Java数据类型。

  另外在概念模型中,两个栈帧作为虚拟机栈的元素,是完全独立的,在大多虚拟机的实现里都会做一些优化处理,令两个栈帧出现一步返重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样在进行方法调用时就可以公用一部分数据,无须进行额外参数赋值传递。

三:方法返回地址

   当一个方法开始执行后,只有两种方式可以退出这个方法。第一种方式是执行引擎遇到任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者,是否有返回值和返回值的类型将根据遇到何种方法返回指令来决定,这种退出方法的方式称为正常完成出口。

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

四:附加信息

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

五:方法调用

      方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本(即调用哪一个方法),暂时还不涉及方法内部的具体运行过程。在程序运行时,进行方法调用是最普遍、最频繁的操作,但前面已经讲过,Class 文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址(相当于之前说的直接引用)。这个特性给 Java 带来了更强大的动态扩展能力,但也使得 Java 方法调用过程变得相对复杂起来,需要在类加载期间,甚至到运行期间才能确定目标方法的直接引用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值