【JVM】虚拟机栈、本地方法栈、程序计数器

Java虚拟机栈

虚拟机栈也是线程私有,而且生命周期与线程相同,每个Java方法在执行的时候都会创建一个栈帧

  • 栈内存为线程私有的空间,每个线程都会创建私有的栈内存

  • 栈空间内存设置过大,创建线程数量较多时会出现栈内存溢出StackOverflowError

  • 栈内存也决定方法调用的深度,栈内存过小则会导致方法调用的深度较小,如递归调用的次数较少

栈帧

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构

  • 栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息

  • 每一个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程

一个线程中方法的调用链可能会很长,很多方法都同时处于执行状态。

  • 对于JVM执行引擎来说,在活动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧

  • 与当前栈帧相关连的方法称为当前方法,定义这个方法的类叫做当前类

执行引擎运行的所有字节码指令都只针对当前栈帧进行操作

  • 如果当前方法调用了其他方法,或者当前方法执行结束,那这个方法的栈帧就不再是当前栈帧了

调用新的方法时,新的栈帧也会随之创建。并且随着程序控制权转移到新方法,新的栈帧成为了当前栈帧

  • 方法返回之际,原栈帧会返回方法的执行结果给之前的栈帧(返回给方法调用者),随后虚拟机将会丢弃此栈帧

关于「栈帧」,我们在看看《Java虚拟机规范》中的描述

  • 栈帧是用来存储数据和部分过程结果的数据结构,同时也用来处理动态连接、方法返回值和异常分派

  • 栈帧随着方法调用而创建,随着方法结束而销毁——无论方法正常完成还是异常完成都算作方法结束

  • 栈帧的存储空间由创建它的线程分配在Java虚拟机栈之中

    • 每一个栈帧都有自己的本地变量表(局部变量表)操作数栈指向当前方法所属的类的运行时常量池的引用

    • 下面我们依次说明这些概念

本地变量表(局部变量表)

  • 是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量

  • 一个局部变量可以保存一个类型为 boolean、byte、char、short、int、float、reference和returnAddress类型 的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经很少使用了

  • 局部变量表的容量以变量槽为最小单位,Java虚拟机规范并没有定义一个槽所应该占用内存空间的大小,但是规定了一个槽应该可以存放一个32位以内的数据类型

  • 在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量。(最大Slot数量)

  • 虚拟机通过索引定位的方法查找相应的局部变量,索引的范围是从 0~局部变量表最大容量,如果Slot是32位的,则遇到一个64位数据类型的变量(如long或double型)时,会连续使用两个连续的Slot来存储

操作数栈

  • 操作数栈(Operand Stack)也常称为操作栈,它是一个 后入先出栈(LIFO)

  • 当一个方法刚刚开始执行时,其操作数栈是空的

  • 随着方法执行和字节码指令的执行,会从局部变量表或对象实例的字段中复制常量或变量写入到操作数栈

  • 再随着计算的进行将栈中元素出栈到局部变量表或者返回给方法调用者

  • 一个完整的方法执行期间往往包含多个这样出栈/入栈的过程

  • 操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2 个栈容量.

  • 同局部变量表一样,操作数栈的最大深度也在编译的时候写入到方法的Code属性的 max_stacks 数据项中。且在方法执行的任意时刻,操作数栈的深度都不会超过 max_stacks 中设置的最大值

动态连接

  • 在一个class文件中,一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池

  • Java虚拟机栈中,每个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用

    • 持有这个引用的目的是为了支持方法调用过程中的动态连接(Dynamic Linking)

  • 这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用 这类转化称为静态解析

    • 另一部分将在每次运行期间转化为直接引用,这类转化称为动态连接

方法返回

  • 当一个方法开始执行时,可能有两种方式退出该方法

    • 正常完成出口

    • 异常完成出口

  • 正常完成出口是指方法正常完成并退出,没有抛出任何异常

    • 如果当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者,或者无返回值

    • 具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令确定

  • 异常完成出口是指方法执行过程中遇到异常,并且这个异常在方法体内部没有得到处理,导致方法退出

    • 无论是Java虚拟机抛出的异常还是代码中使用throw指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会导致方法退出

  • 无论方法采用何种方式退出,在方法退出后都需要返回到方法被调用的位置,程序才能继续执行

    • 方法返回时可能需要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态

    • 方法退出过程实际上就等同于把当前栈帧出栈,因此退出可以执行的操作有

      • 恢复上层方法的局部变量表和操作数栈

      • 把返回值(如果有的话)压入调用者的操作数栈中

      • 调整PC计数器的值以指向方法调用指令后的下一条指令

  • 一般来说,方法正常退出时,调用者的PC计数值可以作为返回地址,栈帧中可能保存此计数值

    • 而方法异常退出时,返回地址是通过异常处理器表确定的,栈帧中一般不会保存此部分信息

附加消息

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

栈异常

Java虚拟机规范中,对该区域规定了这两种异常情况:

  • 如果线程请求的栈深度大于虚拟机所允许的深度,将会抛出 StackOverflowError 异常

  • 虚拟机栈可以动态拓展,当扩展时无法申请到足够的内存,就会抛出 OutOfMemoryError 异常

public class Test {
        private static int index = 1;
        public void call(){
            index++;
            call();
        }
        public static void main(String[] args) {
            Test test = new Test();
            try {
                test.call();
            }catch (Throwable e){
                System.out.println("Stack deep : "+index);
                e.printStackTrace();
            }
        }
}
Stack deep : 21370
java.lang.StackOverflowError
    at com.example.demo.service.Test.call(Test.java:9)

本地方法栈

本地方法栈和虚拟机栈相似,区别就是虚拟机栈为虚拟机执行Java服务(字节码服务),而本地方法栈为虚拟机使用到的Native方法(比如C++方法)服务

本地方法了解

简单地讲,一个Native Method就是一个java调用非java代码的接口

一个Native Method是这样一个java的方法:该方法的实现由非java语言实现,比如C

在定义一个native method时,并不提供实现体(有些像定义一个java interface),因为其实现体是由非java语言在外面实现的

比如:

public class IHaveNatives {
    native public void Native1( int x ) ;
    native static public long Native2() ;
    native synchronized private float Native3( Object o ) ;
    native void Native4( int[] ary ) throws Exception ;
}
  • 这些方法的声明描述了一些非java代码在这些java代码里看起来像什么样子

  • 以下内容读读就行,了解一下即可

标识符native可以与所有其它的java标识符连用,但是abstract除外

  • native暗示这些方法是有实现体的,只不过这些实现体是非java的

  • 但是abstract却显然的指明这些方法无实现体

native与其它java标识符连用时,其意义同非Native Method并无差别

  • 比如native static表明这个方法可以在不产生类的实例时直接调用,这非常方便

  • 比如当你想用一个native method去调用一个C的类库时。上面的第三个方法用到了native synchronized

  • JVM在进入这个方法的实现体之前会执行同步锁机制(就像java的多线程)

一个native method方法可以返回任何java类型,包括非基本类型,而且同样可以进行异常控制

  • 这些方法的实现体可以制一个异常并且将其抛出,这一点与java的方法非常相似

当一个native method接收到一些非基本类型时如Object或一个整型数组时,这个方法可以访问这些非基本型的内部,但是这将使这个native方法依赖于你所访问的java类的实现

  • 有一点要牢牢记住:我们可以在一个native method的本地实现中访问所有的java特性

  • 但是这要依赖于你所访问的java特性的实现,而且这样做远远不如在java语言中使用那些特性方便和容易

native method的存在并不会对其他类调用这些本地方法产生任何影响

  • 实际上调用这些方法的其他类甚至不知道它所调用的是一个本地方法,JVM将控制调用本地方法的所有细节

  • 需要注意当我们将一个本地方法声明为final的情况

  • 用java实现的方法体在被编译时可能会因为内联而产生效率上的提升

  • 但是一个native final方法是否也能获得这样的好处却是值得怀疑的,但是这只是一个代码优化方面的问题,对功能实现没有影响

如果一个含有本地方法的类被继承,子类会继承这个本地方法并且可以用java语言重写这个方法

  • 同样的如果一个本地方法被fianl标识,它被继承后不能被重写

本地方法非常有用,因为它有效地扩充了jvm

  • 有了本地方法,java程序可以做任何应用层次的任务。

为什么要使用本地方法

java使用起来非常方便,然而有些层次的任务用java实现起来不容易,或者我们对程序的效率很在意时

有时java应用需要与java外面的环境交互。这是本地方法存在的主要原因

程序计数器

程序计数器(Program Counter Register),也叫PC寄存器,是一块较小的内存空间

  • 它可以看作是当前线程所执行的字节码指令的行号指示器

  • 字节码解释器的工作就是通过改变这个计数器的值来选取下一条需要执行的字节码指令

  • 分支,循环,跳转,异常处理,线程回复等都需要依赖这个计数器来完成

由于Java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的

  • 在任何一个确定的时刻,一个处理器(针对多核处理器来说是一个内核)都只会执行一条线程中的指令

  • 因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器

  • 各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存

如果一个线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是一个Native方法,这个计数器的值则为空

此内存区域是唯一一个在Java的虚拟机规范中没有规定任何OutOfMemoryError异常情况的区域

.

posted @ 2019-06-04 14:18  鞋破露脚尖儿  阅读(1114)  评论(0编辑  收藏  举报