虚拟机中的运行时栈帧

  每个人都知道,各种各样的动画视频,都是由一帧一帧图片连续切换结果的结果而产生的,其实虚拟机的运行和动画也类似,每个在虚拟机中运行的程序也是由许多的帧的切换产生的结果,只是这些帧里面存放的是方法的局部变量,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成,在虚拟机中包含这些信息的帧称为“栈帧”,每个方法的执行,在虚拟机中都是对应的栈帧在虚拟机栈中的入栈到出栈的过程。其中比较重要的一点时,如果虚拟机中同时有多个线程在执行,那么各个线程的栈帧都是相互独立,互不侵犯的,所以这也实现了局部变量在多线程的环境下也是线程安全的。

一个方法的调用链可能会很长,于是当调用一个方法时,可能会有很多的方法都处于执行状态,但是对于执行引擎来讲,至于位于虚拟机栈顶的栈帧才是有效的,这个栈帧被称为当前栈,这个栈帧所关联的方法称为当前方法,执行引擎的所有指令都是针对当前栈帧进行操作的。

前面已经提到一个栈帧包括局部变量表,操作数栈,动态链接,方法返回地址和一些额外的附加信息组成,接下来对各个部分做一个简单的介绍。

(一)局部变量表

  通过名字可以看出这个里面放的都是局部变量,例如方法参数,方法内部定义的局部变量。一般情况下,在Java程序被编译为class文件的时候这个表的容量最大值就已经确定下来,是存在方法的Code属性的Max_locals数据项中

  在局部变量表中Slot时最小的存储单位,虚拟机规范并没有明确指明一个Slot为多少位,Slot具体的大小也会随着操作系统和虚拟机的不同而不同,一般情况下可以当成时32位来看待,但是规定了一个Slot必须可以存放boolean,byte,char,int,float,reference(可能32位也可能时64位),returnAddress.而对于在虚拟机规范中被明确定义位64位的Long和Double而言,需要用两个连续的Slot来存放,由于时连个Slot来存储,所以在对Long和Double进行操作的时候就会存在原子性的问题,不过虚拟机会对它作出原子性保证(因为每个线程之间的栈帧是相互独立的,所以也不会由线程安全的问题)。

  既然局部变量中存放了很多的局部变量,那么怎么来访问每个变量了?虚拟机规范中指出,虚拟机会利用索引编号的递增来对局部变量表中定义的变量进行依次访问(从0开始),而对于实例方法(非static方法),其局部变量表的第0个索引就是我们熟悉的this,这也是为什么在实例方法中我们可以使用this.name....的原因。

  下面来谈谈Slot对虚拟机的垃圾回收的影响。由于在一个方法中,某个方法内的局部变量的作用范围也不一定可以覆盖整个方法,这就可能导致Slot资源的浪费,如果这个Slot对应的资源足够的大,那么Slot对资源的浪费也就可能会影响到整个虚拟机栈的使用,为了解决这个问题,虚拟机规范中规定了Slot的可重用性,即当一个方法中的某个局部变量超出了变量的有效范围时,那么那个变量的Slot可以被另外一个局部变量来使用。被重用的Slot便失去了和原来堆中实例的联系,这样堆中的实例便可以被垃圾回收器回收,当然一般情况下这些辅助的操作可能对系统性能的提升由很小的影响,但是,如果在那个局部变量“过期”之后还有很多的代码要执行,或者说后面由比较耗时的操作,而且在变量过期前,已经消耗了比较多的系统资源,那么这个辅助动作可能就非常有用了。

  下面将通过三个例子来说明重用Slot对垃圾回收带来的好处:

  示例代码:

public class SlotTest {
        /**
         * 主要验证重复利用Slot对于垃圾回收的帮助
         ×(1)运行参数:-verbose:gc -XX:+PrintGCDetails
         * (2)64M的对象大于了目前年轻代的空间,根据大对象直接进入老年代的原则,在观察结果的时候需要关注ParOldGen
         * */

        public static int M = 1024 << 10;

        public static void main(String[] args) {
                new SlotTest().test2();
        }

        /*
         * replace 在执行gc操作的时候还没有超过它的作用域,也就是堆中还有实例和它直接关联所以不会被回收掉
         *
         * [GC [PSYoungGen: 614K->352K(17856K)] 66150K->65888K(124224K),
         * 0.0024710 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC
         * (System) [PSYoungGen: 352K->0K(17856K)] [ParOldGen:
         * 65536K->65759K(106368K)] 65888K->65759K(124224K) [PSPermGen:
         * 2403K->2401K(21248K)], 0.0102720 secs] [Times: user=0.02 sys=0.00,
         * real=0.01 secs]
         */
        public void test1() {
                // 64M
                byte[] replace = new byte[M << 6];
                System.gc();
        }

        /*
         * 在执行gc时,虽然replace已经过期,但是由于它的Slot中仍然存有相关的局部变量信息,所以gc 还是不可以 对64M的内存进行回收
         *
         * [GC [PSYoungGen: 614K->288K(17856K)] 66150K->65824K(124224K),
         * 0.0019600 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] [Full GC
         * (System) [PSYoungGen: 288K->0K(17856K)] [ParOldGen:
         * 65536K->65758K(106368K)] 65824K->65758K(124224K) [PSPermGen:
         * 2403K->2401K(21248K)], 0.0139210 secs] [Times: user=0.02 sys=0.00,
         * real=0.01 secs]
         */
        public void test2() {
                {
                        byte[] replace = new byte[M << 6];
                }
                System.gc();
        }

        /*在执行gc之前,由于a复用了replace 的Slot,所以此时可以认为replace在堆中的实例没有相关的引用,因此在gc的时候会将它回收
         * [GC [PSYoungGen: 614K->368K(17856K)] 66150K->65904K(124224K),
         * 0.0019430 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC
         * (System) [PSYoungGen: 368K->0K(17856K)] [ParOldGen:
         * 65536K->223K(106368K)] 65904K->223K(124224K) [PSPermGen:
         * 2403K->2401K(21248K)], 0.0107030 secs] [Times: user=0.01 sys=0.01,
         * real=0.01 secs]
         */
        public void test3() {
                {
                        byte[] replace = new byte[M << 6];
                }
                int a = 0;
                System.gc();
        }

}

  对于上面代码中的test3(),也可以用replace=null来达到同样的效果。但是由于赋null值的操作在经过虚拟机JIT编译优化之后就会被消除掉,所以在这种情况下设置null值是没有意义的,其实就是test3()中的做法也是在特殊的情况下才会考虑的做法(后续的方法执行比较耗资源和时间,且前面的操作已经消耗了过多的资源),一般情况下只需要正确的保证每个局部变量有正确的变量作用域就可以了

  最后要说明的是,由于局部变量不像实例变量或类变量那样会在准备阶段或者或者初始化阶段对其进行赋值,所以局部变量在没有赋值的情况下是不可以使用的,如果出现下面的情况,那么编译的时候就会提示“局部变量没有赋值.

public void test4(){
                int a;
                System.out.println(a);
        }

(二)操作数栈:

  首先根据名称可以看出操作数栈是一个基本的栈来实现数据结构,那么它自然也遵守栈的后入先出的原则.其次,它里面主要存放的是一些算数运算用到的参数也可能是中间结果,也可能是在调用其他方法时需要用到的参数,通过这点可以看出,方法刚刚开始执行的时候,这个里面是空的.最后 要说明的是操作数栈中可以存放任意的Java数据类型,包括long和double,且32位的数据类型占一个栈空间,64位的数据类型占2个栈空间.

(三)动态连接:

  在说明什么是动态连接之前先看看方法的大概调用过程,首先在虚拟机运行的时候,运行时常量池会保存大量的符号引用,这些符号引用可以看成是每个方法的间接引用,如果代表栈帧A的方法想调用代表栈帧B的方法,那么这个虚拟机的方法调用指令就会以B方法的符号引用作为参数,但是因为符号引用并不是直接指向代表B方法的内存位置,所以在调用之前还必须要将符号引用转换为直接引用,然后通过直接引用才可以访问到真正的方法,这时候就有一点需要注意,如果符号引用是在类加载阶段或者第一次使用的时候转化为直接应用,那么这种转换成为静态解析,如果是在运行期间转换为直接引用,那么这种转换就成为动态连接。

            

(四)方法的返回地址

  方法的返回分为两种情况,一种是正常退出,退出后会根据方法的定义来决定是否要传返回值给上层的调用者,一种是异常导致的方法结束,这种情况是不会传返回值给上层的调用方法.

  不过无论是那种方式的方法结束,在退出当前方法时都会跳转到当前方法被调用的位置,如果方法是正常退出的,则调用者的PC计数器的值就可以作为返回地址,如果是因为异常退出的,则是需要通过异常处理表来确定.

  在方法的的一次调用就对应着栈帧在虚拟机栈中的一次入栈出栈操作,因此方法退出时可能做的事情包括,恢复上层方法的局部变量表以及操作数栈,如果有返回值的话,就把返回值压入到调用者栈帧的操作数栈中,还会把PC计数器的值调整为方法调用入口的下一条指令。

时间: 2024-07-30 10:19:02

虚拟机中的运行时栈帧的相关文章

深入理解Java虚拟机笔记---运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法的局部变量表,操作数栈,动态连接和方法返回地址等信息.第一个方法从调用开始到执行完成,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程. 每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息.在编译代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的

运行时栈帧结构

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法的局部变量表.操作数栈.动态连接和方法返回地址等信息.每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程.每一个栈帧都包括了局部变量表.操作数栈.动态连接.方法返回地址和一些额外的附加信息.在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入

Jvm(59),虚拟机字节码执行引擎----运行时栈帧结构

后面讲的所有的东西就是对前面所总览的虚拟机栈的进一步理解. 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)[1]的栈元素.栈帧存储了方法的局部变量表.操作数栈.动态连接和方法返回地址等信息.每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程. 每一个栈帧都包括了局部变量表.操作数栈.动态连接.方法返回地址和一些额外的附加信息.在编译程序代码的时候,

浏览器客户端智能自动化:如何取得页面中JavaScript运行时动态生成的URL?

浏览器客户端智能自动化:如何取得页面中JavaScript运行时动态生成的URL? 需求 "页面智能拼接"指的是通过启发式查询DOM树,判断出"下一页"链接,取出其href属性.Chromium的官方插件DOM Distiller完成类似的工作,主要目的就是为了将多页点击流程变成单页的Ajax连续阅读体验. 问题是,现在有些网站为了阻止浏览器客户端这么做,将href属性设置为"#"(或javascript:void()),然后在其onclick事

在Amazon FreeRTOS V10中使用运行时统计信息

在MCU on Eclipse网站上看到Erich Styger在8月2日发的博文,一篇关于在Amazon FreeRTOS V10中使用运行时统计信息的文章,本人觉得很有启发,特将其翻译过来以备参考.原文网址:https://mcuoneclipse.com/2018/08/02/tutorial-using-runtime-statistics-with-amazon-freertos-v10/ FreeRTOS包含一个很好的功能,可以向我提供有关每个任务在系统上运行的时间的信息: Free

深入理解java虚拟机一 JAVA运行时内存区域与class文件

一 JAVA运行时内存区域 JVM在加载class文件时,会将class文件定义的数据结构转为运行时内存中的数据,那么jvm是如何安排运行时的内存区域呢? jvm将运行时内存划分为以下几个部分: 堆:所有线程共享 方法区:类信息.静态变量.常量等 运行时常量池:class文件的常量池(字面常量和符号引用)+运行时产生的常量 程序计数器:  当前线程执行的字节码的行号指示器 虚拟机栈:栈帧 = 本地局部变量表.操作数栈.动态链接.出口信息 本地方法栈:native方法 直接内存:不属于jvm管理,

Java虚拟机学习--记录运行时数据区域

为方便后面学习的理解,记录一下! 运行时数据区 1.线程共享 1.1方法区(Method Area) 1.1.1运行时常量池(Runtime Constant Pool) 1.2堆(Heap) 2.线程私有 2.1虚拟机栈(VM Stack) 2.2本地方法栈(Native Method Stack) 2.3程序计数器(Program Counter Register) 3.直接内存(Direct Memory) 虚拟机栈: 线程私有,生命周期与线程同步,用来执行Java方法. 每个java方法

深入理解Java虚拟机读书笔记---运行时数据区域

运行时数据区域 1.程序计数器 程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器.字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支.循环.跳转.异常处理.线程恢复等基础功能都需要依赖这个计数器来完成.由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令.因此,为了线

Java虚拟机OOM之运行时常量池溢出(5)

如果要向运行时常量池中添加内容,最简单的做法就是使用 String.intern()这个 Native 方法.该方法的作用是:如果池中已经包含一个等于此 String 对象的字符串,则返回代表池中这个字符串的String 对象:否则,将此 String 对象包含的字符串添加到常量池中,并且返回此 String 对象的引用.由于常量池分配在方法区内,我们可以通过-XX:PermSize 和-XX:MaxPermSize 限制方法区的大小,从而间接限制其中常量池的容量代码运行时常量池导致的内存溢出异