精华内容
下载资源
问答
  • 方法返回地址、附加信息一、方法返回地址二、一些附加信息三、虚拟机栈的五道面试题举例栈溢出的情况?调整栈的大小,就能保证不出现溢出吗?分配的栈内存越大越好吗?垃圾回收是否涉及到虚拟机栈?方法中定义的局部...

    一、方法返回地址

    存放调用该方法的PC寄存器的值。
    一个方法的结束,有两种方式

    1. 正常执行完成。
    2. 出现未处理的异常,非正常退出。

    无论哪种方式退出,在方法退出后都返回到该方法被引用的位置。方法正常退出时,调用者的PC寄存器的值作为返回地址,即调用该方法指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般该不会保存这部分信息。
    正常完成出口和异常完成出口的区别在于通过异常完成出口退出的不会给它的上层调用者产生任何的返回值。
    指令返回包含ireturen、dreturn、lreturen、areturn,另外还有一个return指令供声明为void方法、实例方法、类和接口初始化使用。

    二、一些附加信息

    栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如对程序调试提供支持的信息。

    三、虚拟机栈的五道面试题

    举例栈溢出的情况?

    StackOverflowError。

    调整栈的大小,就能保证不出现溢出吗?

    不能,只可以是栈溢出的时间出现的更晚。

    分配的栈内存越大越好吗?

    不是,栈内存分配得越大,其他的内存就会小,也会导致JVM性能降低。

    垃圾回收是否涉及到虚拟机栈?

    不会的。栈只管运行,因此运行完成之后,啥都没有了,所以不需要垃圾回收。

    方法中定义的局部变量是否是线程安全的?

    如果局部变量产生与方法体内,消亡与方法体内,那么就是线程安全的。否则不是。也就是说如果局部变量作为方法参数或者返回值,那么就不是线程安全的。

    展开全文
  • Java虚拟机之虚拟机栈

    2020-06-27 08:02:23
    一、Java虚拟机栈是什么?? Java虚拟机栈(Java Virtual Machine Stack) ,早期也叫Java栈。每个线程在创建时都会创建一虚拟机栈,其内部保存一个个的栈帧(Stack Frame) ,对应着一次次的Java方法调用,是线程私有...

    一、Java虚拟机栈是什么??

    Java虚拟机栈(Java Virtual Machine Stack) ,早期也叫Java栈。每个线程在创建时都会创建一虚拟机栈,其内部保存一个个的栈帧(Stack Frame) ,对应着一次次的Java方法调用,是线程私有的。

    生命周期

    生命周期和线程一致。

    作用

    主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。

    二、Java虚拟机栈的特点

    是一种快速有效的存储方式,访问速度仅次于pc寄存器。JVM直接对Java栈的操作只有两个:每个方法执行,伴随着进栈(入栈、压栈),执行结束后的出栈工作,对于栈来说不存在垃圾回收问题。


    三、栈中可能出现的异常
    Java虚拟机规范允许Java栈的大小是动态的或者是固定不变的。如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个OutOfMemoryError 异常。

    四、设置栈的大小

    栈的大小决定了函数调用可达的最大深度,我们可以使用-Xss选项来设置栈的最大空间。

    五、栈中存储着什么?

    每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在。在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame )。栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

    六、栈的运行原理

     

    JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧。(Current Frame) ,与当前栈帧相对应的方法就是当前方法(Current。Method),定义这个方法的类就是当前类(Current Class)。执行引擎运行的所有字节码指令只针对当前栈帧进行操作。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。

    不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令; 另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。

     

     

     七、栈帧的内部结构

                                                       

     八、局部变量表

    局部变量表也被称之为局部变量数组或本地变量表,定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference) ,以及returnAddress类型。由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。

    局部变量表,最基本的存储单元是Slot (变量槽),局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress 类型的变量。局部变量表里,32位以内的类型只占用一个slot (包括returnAddress类型),64位的类型(long和double)占用两个slot。byte、short 、char在存储前被转换为ihnt, boolean 也被转换为int,0表示false,非0表示true。
    long和double则占据两个Slot。

     

    JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值,当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot 上。如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类 型变量)。如果当前帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。

                               

     九、操作数栈

    每一个独立的栈帧中除了包含局部变量表以外,还包含一个后进先出(Last-In-First-Out)的操作数栈,也可以称之为表达式栈

    (Expression Stack) 。操作数栈在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push) /出栈(pop)。某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如:执行复制、交换、求和等操作。

     

    操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的。每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为max_ stack的值。
    栈中的任何一个元素都是可以任意的Java数据类型。32bit的类型占用一个栈单位深度,64bit的类型占用两个栈单位深度。操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈(push)和出栈(pop) 操作来完成一次数据访问。
     

    十、动态链接

    每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接(Dynamic Linking) 。比如: invokedynamic指令,在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Symbolic Reference)保存在class文件的常量池里。比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

    十一、方法返回地址

    存放调用该方法的pc寄存器的值。一个方法的结束,有两种方式:正常执行完成或者出现未处理的异常,非正常退出。无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。

    在方法执行的过程中遇到了异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出。简称异常完成出口。方法执行过程中拋出异常时的异常处理,存储在一一个异常处理表,方便在发生异常的时候找到处理异常的代码。

    本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表,操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

    十二、附加信息

    一些java虚拟机实现有关的附加信息。

     

     

     

     

    展开全文
  • Native Method Stack两种架构模型简介虚拟机栈概述特点区别与堆生命周期可能出的异常(错误)栈的存储单位栈帧的内部结构演示代码字节码执行过程图示本地方法栈概述(Native Method Stack)本地方法 两种架构模型简介 ...

    两种架构模型简介

    1. 栈的指令架构模型: 用于资源受限的系统, 操作只有入栈(压栈)和出栈, 使用0地址指令, 因此无需考虑地址分配
    • 优点: 移植性强适合跨平台设计
    • 缺点: 指令集小(每8位进行对齐), 所以每次操作需要更多的指令, 因此性能上不如寄存器架构模型
    1. 寄存器架构模型: 典型的应用是x86的二进制的指令集 如传统 PC, Android的 davlik虚拟机. 指令集完全依赖于硬件, 是 CPU直接执行的
    • 优点: 效率高
    • 缺点: 移植性差, 硬件耦合度高, 16位指令对齐

    虚拟机栈概述

    • Hotspot虚拟机是基于栈的指令架构. JVM支持多个线程并行的执行, 每个线程都对应一份虚拟机栈, 也是线程私有的. 主要作用是主管程序的运行

    特点

    • 操作方式只有入栈和出栈
    • 没有垃圾回收的问题

    区别与堆

    • 栈主要负责程序的运行, 而堆的主要作用是存储
    • 栈存储方法调用时产生的局部变量(如 8种基本数据类型, 对象引用地址), 而堆存储对象的实例
    • 栈没有 GC, 而堆有 GC
    • 栈是不同线程间是隔离的, 而堆是共享的

    生命周期

    • 虚拟机栈是随线程的开始和结束而创建和销毁

    可能出的异常(错误)

    栈大小可以是固定大小, 也可以动态扩展

    • 固定大小时, 一旦超出了栈的大小, 就会抛出 StackOverflowError(栈溢出异常)
    • 当可以动态扩展时, 一旦没有足够的内存用于扩展, 就会抛出 OutOfMemoryError(内存溢出异常)
      * 配置选项: -Xss1m设置线程栈深度
      * 更多调优相关参考: https://blog.csdn.net/qcl108/article/details/103476424

    栈的存储单位

    • 栈中保存的一个个栈帧(Stack Frame), 每个栈帧对应一个方法, 栈帧是栈的存储单位, 栈帧的大小影响栈的深度

    栈帧的内部结构

    • 局部变量表(Local Variables)
    • 操作数栈(Operand Stack), 又称表达式栈(Expression Stack)
    • 动态链接(Dynamic Linking), 又称指向运行时常量池的方法引用
    • 方法返回地址(Return Address)
    • 附加信息
      在这里插入图片描述
    1. 局部变量表
    • 主要存储方法的形参和方法体内的局部变量
    • 局部变量表又称局部变量数组(一个数字数组), 基本存储单元是 Slot(变量槽), 32位以内的数据类型只占一个 Slot(如 byte, short, char, boolean, float, int和 returnAddress类型), 64位类型占用两个 Slot(如 long, double). 通过索引可以获取变量值, 注 获取64位类型变量值时, 只需使用起始索引即可
    • 线程私有, 所以本身是不存在数据安全问题的, 不过多个线程修改指定线程上的变量时, 如果不考虑同步问题, 也会导致线程不安全的, 还有通过形参或返回(作用域不只在方法内部)的 StringBuilder变量也会导致线程不安全
    • 局部变量表所需要的容量大小是在编译时已确定下来的(就是局部变量数字数组的长度), 并保存在方法的 code属性下的 maximum local variables数据项中. 在方法运行期间是不会再改变局部变量表的大小
    • 方法的嵌套调用次数由栈的大小来决定. 一般, 栈越大, 方法嵌套调用次数越多, 然后形参和局部变量大小决定栈帧的大小, 同时会影响方法嵌套调用次数
    • 局部变量表中的变量只在当前方法调用中有效. 当方法执行结束时, 随着方法栈帧的销毁, 局部变量表也会随之销毁
    1. 操作数栈
    • 开始执行了一个方法, 会随之创建一个新的栈帧, 然后根据字节码指令, 往栈中写入或提取数据, 即入栈/出栈, 在方法体内再调用了其它方法同时有返回值的话, 其返回值将会被压入当前栈帧的操作数栈中
    • 主要用于保存计算过程的中间结果, 作为计算过程中的变量临时存储空间
    • 操作数栈是数组结构, 不过不能通过索引访问, 而只能通过出入栈方式访问数据. 其栈大小(深度)是编译时已设定好的

    演示代码

    
    public class TestStack {
        public void testMethod() {
            byte a = 15;
            int b = 10;
            int c = a + b;
        }
    
        public int testSum() {
            int a = 10;
            int b = 1000;
            int c = a + b;
            return c;
        }
    
        public void getTestSum() {
            int d = testSum();
            int e = 10000;
        }
    }
    
    

    字节码执行过程图示

    在这里插入图片描述
    在这里插入图片描述

    1. 动态链接
    • Java源文件被编译到字节码文件时, 将所有的变量和方法引用都作为符号引用保存在字节码文件的常量池里. 动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用(为了指令便于识别, 又剩空间)
    • 程序运行后, 将字节码文件中的常量池(Constant pool)放到方法区的运行时常量池里
    • 每个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用. 包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接
    1. 方法返回地址
    • 保存调用方法的 PC寄存器的值方法返回地址
    • 一个方法的退出, 有两种方式: 正常退出和非正常退出(异常退出)
    • 无论通过那种方式退出, 在方法退出后都返回到该方法被调用的位置. 正常退出时, 当前 PC计数器的值作为返回地址. 而异常退出时, 不会返回任何值
    1. 附加信息
    • 虚拟机规范中, 栈帧中还允许携带与 Java虚拟机实现相关的, 对程序调试提供支持的信息

    本地方法栈概述(Native Method Stack)

    并不是所有的 JVM都支持本地方法, 因为 Java虚拟机规范上, 并没有明确要求本地方法的使用语言和具体实现方法. Hotspot VM是本地方法栈和虚拟机栈合二为一的虚拟机

    • 本地方法栈是管理本地方法运行的, 本地方法是通过 C语言实现的, 在 Execution Engine执行时加载本地方法库(Native Method Library)
    • 与虚拟机站相同: 没有 GC, 不同线程间是隔离的(线程私有的)

    本地方法

    • 当调用了一个本地方法时, 就会进入不再受虚拟机限制的环境, 级别与虚拟机一样, 所以可以访问任何虚拟机内部的运行时数据区
    • 可以直接使用本地处理器的寄存器
    • 可以直接使用本地内存

    如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

    展开全文
  • 虚拟机栈3.1概念2. 栈内存溢出3.3 存储结构和原理3.4 内部结构4. 局部变量表4.1 变量槽Slot4.2 静态变量 VS局部变量5. 操作数栈3.4.5 栈顶缓存技术6. 动态链接6.1 概念6.2 方法的调用7. 方法返回地址8. 附加信息9. ...


    运行时数据区

    1. 内存和线程

    在这里插入图片描述

    Java的内存布局规定了Java在运行过程中内存的申请、分配、管理的策略,保证了JVM高效稳定的运行。JVM中关于内存的管理主要涉及的就是运行时数据区,如下所示:


    在这里插入图片描述

    其中有些部分是线程独有的,如虚拟机栈、本地方法栈和程序计数器;而方法区或是元空间、堆是进程所有的,即所有线程所共享的。对于JVM来说,由于堆通常占据了最大的内存空间,因此针对于JVM的优化实际上就是对于堆空间的优化,还有一小部分是对方法区的优化

    计算机中的多个进程并发的执行,同时进程中的多个线程也可以并发执行,其中进程中的每个线程和操作系统的本地线程是直接映射的。本地线程会随着Java线程的创建而创建,最后会随着Java线程的结束而终止。当在Java程序中创建好线程后,实际上是操作系统负责将所有的线程和本地线程相互映射,然后将本地线程调度到可用的CPU上。一旦本地线程初始化完成,它就会调用Java线程中的run()

    此外,JVM中的系统级线程主要包括:

    • 虚拟机线程:它需要JVM到达安全点才会出现,这种此案成的执行类型包括"stop-the-world"的垃圾收集、线程栈收集、线程挂起和偏向锁撤销
    • 周期任务线程:它是时间周期事件的体现,一般用于周期性操作的调度执行
    • GC线程:JVM中不同的垃圾收集机制提供了不同的支持
    • 编译线程:它负责在运行时间字节码编译到本地代码
    • 信号调度线程:它用于接收信号并发送给JVM,在它内部通过调用适当的方法进行处理

    2. 程序计数器

    在这里插入图片描述

    JVM中的程序计数器(Program Counter Register,PC Register)和计算机中的PC寄存器的原理是类似的,它负责存放线程和指令的现场信息,即指向下一条指令的地址,执行引擎会根据地址来执行相应的指令。PC Register具有如下的特点;

    • 只占用很小的内存空间,但运行速度最快
    • 线程私有,它的生命周期和所属的线程是一致的,JVM使用它进行并发执行的线程之间的切换
    • 任何时间一个线程只有一个方法在执行,即当前方法。程序计数器会存储当前线程正在执行的Java方法的JVM指令地址,如果是正在执行的native方法,则是undefined
    • 它是程序控制流的指示器,Java中的分支、循环、跳转、异常处理、线程恢复都需要程序计数器来完成
    • 字节码解释器工作时也依赖程序计数器来选择下一条需要执行的字节码指令
    • JVM中唯一一个没有规定任何OOM异常的区域

    下面我们通过一个demo来看一下程序计数器在JVM的运行期间是如何起作用的,假设此时程序如下所示:


    在这里插入图片描述

    当我们编译结束后,使用javap - v xxx.class命令或是jclasslib插件查看程序所对应的字节码信息时,ByteCode部分包含两部分的信息:指令地址和相应的指令。程序计数器中保存的就是指令地址,执行引擎根据指令地址来取指令执行。


    3. 虚拟机栈

    3.1概念

    JVM为了做到真正的跨平台性,实现了底层硬件之间的解耦,采用了栈的思想来设计。这样的做法可以实现跨平台、指令集小。但由于不依赖于寄存器,不足之处在于性能下降,执行相同的操作需要更多的指令。

    栈和堆的区别

    • 栈是运行时单位,堆是存储的单位。栈解决程序的运行问题,即程序如何执行,堆处理数据的存储问题,即数据应该如何存放
    • 对象主要方法堆空间中,它在运行时数据区中所占比例较大
    • 栈空间中存放基本数据类型的局部变量,以及引用类型对象的引用,即对象在堆空间中的地址

    Java虚拟机栈(Java Virtual Machine Stack)是线程私有的,每个线程在创建时都会创键一个属于自己的虚拟机栈,栈内部保存一个个的栈帧(Stack Frame),其中每个栈帧都对应一个Java方法。它主要负责Java程序的运行,它保存方法的局部变量和部分结果,并参与方法的调用的返回。

    栈是一种后进先出的结构,因此,当方法被调用时入栈,当方法调用结束后出栈,它就不存在垃圾回收问题。


    3.2 栈内存溢出

    栈内存可以分为虚拟机栈(VM Stack)和本地方法栈(Native Method Stack),除了它们分别用于执行Java方法(字节码)和本地方法,其余部分原理是类似的(以虚拟机栈为例说明)。Java虚拟机栈是线程私有的,当线程中方法被调度时,虚拟机会创建用于保存局部变量表、操作数栈、动态连接和方法出口等信息的栈帧(Stack Frame)。

    具体来说,当线程执行某个方法时,JVM会创建栈帧并压栈,此时刚压栈的栈帧就成为了当前栈帧。如果该方法进行递归调用时,JVM每次都会将保存了当前方法数据的栈帧压栈,每次栈帧中的数据都是对当前方法数据的一份拷贝。如果递归的次数足够多,多到栈中栈帧所使用的内存超出了栈内存的最大容量,此时JVM就会抛出StackOverflowError。

    下面我们下一个不断的递归调用自己的方法,然后执行该程序:

    public class StackOverflowErrorDemo {
        private static int stackLength = 0;
        public static void main(String[] args) {
            StackOverflowErrorDemo demo = new StackOverflowErrorDemo();
            try {
                demo.pusStack();
            } catch (Throwable e){
                System.out.println("stack length is: " + demo.stackLength);
                throw e;
            }
        }
        public void pusStack(){
                stackLength++;
                pusStack();
        }
    }
    

    运行程序很快就会抛出异常,异常信息如下所示。从输出信息中发现,出现问题的地方就是程序中递归调用方法自身的地方。

    stack length is: 20315
    Exception in thread "main" java.lang.StackOverflowError
    	at OutOfMemoryErrorDemo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:16)
    	at OutOfMemoryErrorDemo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:16)
    	at OutOfMemoryErrorDemo.StackOverflowErrorDemo.pusStack(StackOverflowErrorDemo.java:16)
        ......
    

    总之,不论是因为栈帧太大还是栈内存太小,当新的栈帧内存无法被分配时,JVM就会抛出StackOverFlowError。通常栈内存可以通过设置-Xss参数来改变大小。

    如果Java虚拟机栈是允许动态扩展的,并且在尝试扩展时无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那么JVM会抛出OOM异常。

    3.3 存储结构和原理

    前面说到虚拟机栈是线程私有的,不同线程中所包含的栈帧是不允许相互引用的。线程正在执行的每个方法都对应于栈中的一个栈帧。栈帧中保存了方法在执行过程中的各种数据信息,当方法被执行时,方法对应的栈帧入栈;当方法执行结束后,栈帧出栈。因此,在每个时间点上,虚拟机栈中只会有一个活动的栈帧(栈顶栈帧,当前栈帧),它对应于当前正在执行的方法(当前方法)。

    执行引擎运行的所有字节码指令只针对于当前栈帧进行操作。如果当前方法调用了其他方法,方法返回之际,当前栈帧会返回此方法的执行结果给前一个栈帧,然后虚拟机会丢弃当前栈帧,使得前一个栈帧重新称为栈顶栈帧。

    Java方法有两种返回函数的方式:

    • 正常的返回,使用return指令
    • 抛出异常

    但不管使用使用哪一种方式,方法执行结束后都会弹出当前栈顶栈帧。


    在这里插入图片描述

    3.4 内部结构

    每个栈帧都存储了如下的内容:

    • 局部变量表(Local Variable)
    • 操作数栈(Operand Stack)
    • 动态链接(Dynamic Linking)
    • 方法返回地址(Return Adress)
    • 一些附加信息

    4. 局部变量表

    局部变量表也被称为局部变量数组或本地变量表,它的形式为数字数组,主要用于存储方法参数和定义在方法体内部的局部变量,这些数据类型包括各种基本数据类型、对象引用和returnAddress类型。它所需的容量大小是在编译期确定下来的,并保存在方法的Code属性maximum local variables数据项中,在方法执行期间它的大小是不会改变的。

    局部变量表是与栈调优最为相关的部分,方法执行时,JVM通过局部变量来完成方法的传递。而且它也是垃圾回收的根节点,只要被局部变量表中直接或是间接引用的对象都不会被回收。

    局部变量表的大小也影响了方法的递归调用,当一个方法的参数和局部变量越多时,它对应的局部变量表就会越大,栈帧也会越大,那么执行该方法所需的栈内存空间就越大,最后可递归调用的次数就越小。

    而且局部变量表中的变量只在当前方法执行中有用。在方法执行时,JVM通过使用局部变量表来完成参数值到参数变量列表的传递过程;当方法调用结束后,随着栈帧的销毁,栈帧中的局部变量表也就不复存在了。


    在这里插入图片描述

    例如在之前的例子中,我们通过jclasslib打开mian()的局部变量表,可以看到表中的元素和方法中的变量是一一对应的。局部变量表中的信息包含变量的长度、表中的起始索引、名字和相应的描述信息。

    4.1 变量槽Slot

    局部变量表中参数值的存放总是以局部变量数组的索引0开始,到数组的长度 - 1 终止,其中局部变量表最基本的存储单元就是Slot。槽中可存放编译期可知的各种基本数据类型、引用类型和returnAddress类型的变量,除了long和double类型的变量占两个槽之外,其他的变量都只占一个槽。

    • byte、short、char、float在存储前都被转换为int,boolean用0表示false,用非0表示true
    • long和double占两个槽

      在这里插入图片描述

    JVM会为局部变量表中的每个槽分配一个起始索引,通过索引就可以访问局部变量表中指定的变量值。从上面的demo中可以看出,当调用一个方法时,方法的参数列表和方法体中的的变量会按序的插入槽中。如果当前栈帧是构造方法或是实例方法对应的栈帧,那么该对象引用this也会被插入到槽中,并且起始索引就是0,其他的依然接着按序插入。

    此外,栈帧中的局部变量表中的Slot是被重复利用的,如果一个变量过了其作用域,那么在其作用域之后声明的局部变量就可能重用它所占的slot。例如:

    private void test() {
            int a = 0;
            {
                int b = 0;
                b = a+1;
            }
            //变量c使用之前以及经销毁的变量b占据的slot位置
            int c = a+1;
        }
    
    4.2 静态变量 VS局部变量

    变量按照它在类中声明的位置可分为:

    • 成员变量:使用前都会被赋予默认初始值
      • static修饰:它属于类变量,在类加载的linking阶段会为类变量赋初始值,在初始化阶段会为类变量显式的赋值
      • 不被static修饰
    • 局部变量:使用前需显式的进行赋值

    5. 操作数栈

    每个独立的栈帧除了虚拟机栈之外,还包含一个后进先出的操作数栈(表达式栈)。每个操作数栈都有一个明确的表示栈深度的数值,其所需的最大深度在编译期确定,保存在方法的code属性的max_stack值中。栈中的数据可以是任意的数据类型,而且不同类型数据在栈中所占的大小是不同的:

    • 32bit的类型数据占用一个栈单位深度
    • 64bit的类型数据占用两个栈单位深度

    由于栈本身的结构特点,当程序想要取栈中的数据时只能通过入栈和出栈操作进行获取。

    操作数栈主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。它会随着栈帧的创建而创建,而且在创建的一开始为空,随着方法的执行,栈中的数据会随着发生变化。它在方法的执行过程中用于根据字节码指令往栈中写入或是提取数据,基本操作就是入栈和出栈:

    • 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出,使用它们后再把结果压栈
    • 涉及操作数栈的常用操作有:复制、交换、求和等

    此外,如果被调用的方法带有返回值,那么返回值将会被压入当前栈帧的操作数栈中,并更新程序计数器中下一条需要执行的字节码指令地址。

    操作数栈中元素的数据类型必须和字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据流分析阶段再次验证。

    下面我们通过一个例子来看一下操作数栈在方法的执行过程中是如何发生变化的,假设此时只是做一个两数相加的操作,代码如下所示:

    public class StackDemo {
        public static void main(String[] args) {
            int a = 2;
            int b = 3;
            int res = a + b;
    
            System.out.println(res);
    
        }
    }
    

    编译结束后通过jclasslib查看字节码指令:


    在这里插入图片描述

    代码对应的字节码指令为:

    0 iconst_2
    1 istore_1
    2 iconst_3
    3 istore_2
    4 iload_1
    5 iload_2
    6 iadd
    7 istore_3
    8 return
    

    其中的iconst指令表示变量的定义,istore表示入栈操作,iload表示出栈操作。下面我们通过图示的方法具体的来看一下栈发生的变化。


    在这里插入图片描述

    而实际中局部变量表的存放和图示的有所不同,这里只是为解释方便所画。实际中,局部变量表的内容是:

    LocalVariableTable:
        Start  Length  Slot  Name   Signature
        0       9     0  args   [Ljava/lang/String;
        2       7     1     a   I
        4       5     2     b   I
        8       1     3   res   I
    
    
    3.4.5 栈顶缓存技术

    基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作所需的入栈和出栈指令通常更多,这就意味着将需要更多的指令分派次数和内存读写次数。频繁的读写内存必然会影响执行速度,因此,为了解决这个问题,HotSpot虚拟机中引入了栈顶缓存技术。它就是将栈顶元素全部缓存到物理CPU的寄存器中,以此来降低对内存的读写次数,从而提升程序的执行效率。


    6. 动态链接

    6.1 概念

    Java程序经过编译得到字节码文件后,所有的变量和方法引用都被表示为符号引用(Symbolic Reference)保存在字节码文件的常量池中。当某个方法调用另一个方法时,就是通过常量池指向该方法的符号引用完成的。虚拟机栈中的每一个栈帧内部都包含一个指向运行时常量池或该栈帧所属方法的引用,包含这个引用的目的是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。因此,动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。


    在这里插入图片描述

    假设当前代码为:

    public class ConstantPoolDemo {
        public static int number = 1;
    
        public static void main(String[] args) {
            int res = number + 1;
            System.out.println(res);
        }
    }
    

    经过编译得到的字节码文件信息如下所示:


    在这里插入图片描述

    下面分析以下字节码指令和运行时常量池中所包含的信息:

    • 0: getstatic #7指令首先获取定义的静态常量number,此时的符号引用为#7。因此,我们需从运行时常量池中找到它指向的位置#7 Fieldref,它是一个字段引用,又指向了#8、#9
    • #8 Class #10它表示我们定义的类,同时指向到#10,即定义的ConstantPoolDemo类
    • 返回去我们再看#9 NameAndType,表示此时变量为int型。同时指向了#12#13#12就表示变量的类型为int
    • #13此时已经走到了6: getstatic#13指向了#14#15#14指向了#16#15指向了#17#18都表示调用打印流PrintStream中println()的过程

    6.2 方法的调用

    • 静态链接:当一个字节码文件被装载进JVM时,如果被调用的目标方法在编译期可知,且运行期保持不变时,将调用方法的符号引用转换为直接引用的过程称为静态链接
    • 动态链接:如果被调用的方法在编译器无法被确定下来,只能在运行期进行转换操作,由于这种方式具有动态性,所以将其称为动态链接

    JVM中将符号引用转换为调用方法的直接引用和方法的绑定机制有关,对应的方法的绑定机制为早期绑定(Early Binding)和晚期绑定(Late Binding):

    • 早期绑定:指调用的方法如果编译期可知,且运行期保持不变时,即可将这个方法与所属的类型就行绑定。这样由于明确了被调用的目标方法,因此可以使用静态链接的方式将符号引用装换为直接引用
    • 晚期绑定:如果被调用的方法只能在运行期根据实际的类型进行绑定相关的方法的话,这种方式就是晚期绑定

    7. 方法返回地址

    方法返回地址(Return Address)存放的就是调用该方法的程序计数器值,由3.3中的内容可知,无论程序因何种方式退出,在方法退出后都会返回到该方法被调用的位置。当程序正常退出时,调用者的程序计数器中的值作为返回地址,即调用该方法的下一条指令的地址。而当因为异常退出时,返回地址需要通过异常表确定,栈帧中一般不会保存这部分信息。

    正常完成退出和因异常而退出的区别在于:因为异常退出的不会给它的上层调用者产生任何的返回值。


    在这里插入图片描述

    方法的退出本质上就是对应栈帧出栈的过程,此时需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置程序计数器值等,让调用者方法继续执行。

    具体来说,一个方法开始执行后,只有两种方式可以退出:

    1. 执行引擎遇到任意一个方法返回的字节码指令,会有返回值传递给上层的方法调用者,称为正常完成出口
    • 方法在正常调用完成之后,究竟使用哪一个返回指令还需要根据方法的返回值的实际数据类型确定
    • 字节码指令中,返回指令包含ireturn(当返回值是boolean、byte、char、short和int)、lreturn、freturn、dreturn、areturn和return(声明为void的方法、实例初始化方法、类或接口的初始化方法)。
    1. 方法的执行过程中遇到了异常,并且该异常没有在方法内进行处理,即只要在方法的一场表中没有搜索到匹配的异常处理器,就会导致方法退出,称为异常完成出口

    8. 附加信息

    栈帧中还允许携带一些与JVM实现相关的附加信息,例如,对程序调试提供支持的信息等。


    9. 本地方法栈

    一个本地方法就是Java程序调用非Java代码的接口,它的实现不是由Java语言实现。当Java程序需要和外间需要交互时,就可以选择使用本地方法。

    public  class IHaveNatives {
        public native void Native1(int x);
    
        native static public long Native2();
    
        native synchronized private float Native3(Object o);
    
        native void Native4(int[] array) throws Exception;
    
    }
    

    本地方法栈(Native Method Stack)用于本地方法的调用,线程私有,它允许被实现成固定大小或是可动态扩展的内存大小。程序使用本地方法栈的具体做法是:在本地方法栈中登记native方法,在执行引擎执行时加载本地方法库。当某个线程调用一个本地方法时,它就进入了一个全新的且不受虚拟机限制的世界,它和虚拟机拥有同样的权限。

    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
    • 甚至可以直接使用本地处理器中的寄存器
    • 直接从本地内存堆中分配任意数量的内存

    但并不是所有的JVM都支持本地方法,HotSpot直接将本地方法栈和虚拟机栈合二为一。

    展开全文
  • 虚拟机栈 1、此区域是线程私有的,生命周期与线程相同。每个方式执行会创建一个栈桢,一个栈桢又包括局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息,其中动态链接、方法返回地址和一些附加信息合起来...
  • Java虚拟机栈 Java虚拟机栈它也是线程私有的一块内存区域,所以生命周期自然和线程相同。 每个方法在执行的时候,Java虚拟机栈都会同步生成一个栈帧,然后再将这个栈帧压入Java虚拟机栈中,所以Java虚拟机栈主要保存...
  • 方法返回地址 通过上一篇文章,我们大体了解了JVM的整体架构,其分为:元数据(JDK7是方法区)、堆、虚拟机栈、本地方法栈、程序计数器几个部分。 本篇文章,咱们对虚拟机器栈进行剖析,一探究竟。 1.什么是虚拟机...
  • 栈帧是虚拟机栈的基本单位,栈帧的调入对应着方法的调用,栈帧的弹出对应着方法的结束返回,其中,由于只有弹栈和入栈的操作,java虚拟机栈没有GC机制,但在栈空间不够时会出现StackOflowError错误,java虚拟机允许...
  • 线程与虚拟机栈

    2019-10-09 02:25:50
    一、什么是虚拟机栈 虚拟机栈:与程序计数器、本地方法栈都是属于线程私有的JVM内存区域。虚拟机栈的生命周期是和线程相同的,是在JVM运行时创建的,在线程中,方法在执行的过程中会创建一个栈帧(Stack Frame)。...
  • 虚拟机栈(JVM Stack)的介绍 与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java...
  • JVM学习-04:JVM之虚拟机栈详解

    千次阅读 2020-07-12 00:39:08
    虚拟机栈概念 1.概念 由于跨平台性的设计, Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。...
  • 方法返回地址 局部变量表 可以将局部变量表理解为一个数组,数组中的内容为所在的栈帧对应的方法上参数以及方法体内的局部变量,可以有基本数据类型、对象引用(真正的对象在堆上)。 既然是数组,而数组的长度是...
  • JVM 虚拟机栈详解

    千次阅读 2017-11-28 17:01:17
    Java 虚拟机都会分配一个虚拟机栈,Java虚拟机栈是以帧为单位来保存线程的运行状态。Java栈只会有两种操作:以帧为单位进行压栈跟出栈。 某个线程正在执行的方法称为当前方法,以此类推出当前类,当前常量池(每一...
  • JVM以方法作为最基本的执行单元,栈帧则是用于支持虚拟机进行方法调用与方法执行背后的数据结构,同样它也是JVM运行时数据区中的虚拟机栈的栈元素。
  • 深入理解Java虚拟机栈的栈帧

    万次阅读 多人点赞 2019-08-25 00:05:48
    本节将会介绍一下Java虚拟机栈中的栈帧,会对栈帧的组成部分(局部变量表、操作数栈、动态链接、方法出口)分别进行介绍,最后...方法的返回地址 结合javap命令理解栈帧 Java虚拟机栈概述 Java虚拟机栈(Java Vir...
  • 虚拟机栈整体结构

    2020-10-09 22:09:06
    java虚拟机栈,早期也叫java栈,每个线程在创建的时候都会创建一个虚拟机栈,其内部保存一个个的栈帧(stack frame),一个栈帧对应一个java方法调用。 与PC寄出去一样是线程私有的。 生命周期 生命周期与线程一致。...
  • 1、什么是java虚拟机栈: 栈其实是有数组和链表实现的,一种先进后出的数据结构,每个线程在创建的时候,都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame)对应着一次方法的调用,线程私有,栈帧是栈...
  • 如果在类型C中找到与常量池中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束:如果不通过,则返回IllegalAccessError异常。 否则按照继承关系从下往上一次对...
  • jvm虚拟机栈的作用

    2019-09-20 23:52:53
    jvm虚拟机栈的作用 jvm虚拟机栈栈帧的组成 jvm虚拟机栈,也叫java栈,它由一个个的栈帧组成,而栈帖由以下几个部分组成 局部变量表-存储方法参数,内部使用的变量 操作数栈-在变量进行存储时,需要进行入栈和出栈 ...
  • 虚拟机栈 虚拟机栈概述 由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计为基于寄存器的。 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多...
  • 虚拟机栈+堆

    2020-01-03 16:52:01
    虚拟机栈和栈帧 每个栈帧中包括局部变量表(Local Variables)、操作数栈(Operand Stack)、指向运行时常量池的引用(A Reference to the run-time constant pool)、方法返回地址(Return Address)、附加信息...
  • 1.虚拟机栈(VM Stack)结构图 2.虚拟机栈帧栈是什么? 虚拟机用来进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。 3.栈帧的组成 栈帧存储了方法的局部...
  • JVM虚拟机栈 动态链接(指向运行时常量池的方法引用) 每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用。包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接。比如invokedynamic指令...
  • Java虚拟机栈详解

    2021-01-23 22:44:38
    栈帧包括局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息 每个方法被调用至执行完毕的过程,就对应这个栈帧在虚拟机栈中从入栈到出栈的完整过程 栈的深度有限制 局部变量表 局部变量表(Local ...
  • 栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。 每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。 局部变量表: 局部变量表是一组变量值存储...
  • 是运行时的单位,而堆是存储的单位。 即:解决程序的运行问题,即程序如何...主管Java程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用地址)、部分结果,并参与方法的调用和返回。 变量的分类:按
  • jvm-java虚拟机栈详解

    2020-05-27 15:37:46
    java虚拟机栈虚拟机栈存储内容栈帧Local Variables 局部变量表Operand Stacks 操作数栈Dynamic Linking 动态链接Invocation/Completion 方法返回地址例子对java文件进行分析对calc()方法分析 虚拟机栈存储内容 栈帧...
  • 虚拟机栈1.1 优点1.2 Java虚拟机可能出现的异常1.3 设置栈内存大小1.4 栈存储的内容1.5栈运行原理 1. 虚拟机栈 优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。 栈是...
  • java虚拟机栈

    2021-09-22 15:14:40
    java虚拟机栈1.java虚拟机栈2.栈帧(Stack Frame)3.局部变量表4. 变量槽(Variable Slot)5.reference(对象实例的引用)6. 动态链接7. 方法出口 1.java虚拟机栈 java虚拟机栈是线程私有的, 生命周期与线程相同。 ...
  • Java虚拟机栈

    2019-03-29 15:15:02
    一 Java虚拟机栈概念。 Java虚拟机栈中存储的内容,它用于存储数据和部分过程结果的数据结构,同时也被用来处理动态链接/方法返回值和异常分派. 一个完整的栈帧包括:局部变量表/操作数栈/动态链接信息/方法正常完成...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 80,901
精华内容 32,360
关键字:

虚拟机栈返回地址