文章目录

    • 一、Java虚拟机栈
      • 1.1 局部变量表
      • 1.2 操作数栈
      • 1.3 动态连接
      • 1.4 方法的返回地址
      • 1.5 栈异常
    • 二、本地方法栈
      • 2.1 本地方法栈的作用
      • 2.2 为什么需要本地方法栈?

一、Java虚拟机栈

虚拟机栈线程私有,生命周期与线程相同,每个Java方法在执行时都会创建一个栈帧(Stack Frame)。

  • 栈帧是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟机栈的基本元素。每一个方法从调用到返回都对应着一个栈帧入栈出栈的过程。最顶部的栈帧称为当前栈帧,栈帧所关联的方法称为当前方法,定义这个方法的类称为当前类,当前线程中虚拟机只会对当前栈帧进行操作。
  • 栈帧的作用有存储数据,部分过程结果,处理动态链接,方法返回值和异常分派。
  • 每一个栈帧包含的内容有局部变量表、操作数栈、动态链接、方法返回地址和一些额外的附加信息。

1.1 局部变量表

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

  • 实例方法局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用”this”。其余参数则按照参数表的顺序来排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。
  • 例如:method(int a1,int a2),参数表为a1和a2,则局部变量表索引0、1、2则分别存储了this指针、a1、a2,如果方法内部有其他内部变量,则在局部变量表中存在a2之后的位置。
  • 局部变量不存在”准备阶段”。因此没有默认值,如果一个局部变量定义了但没有赋初始值是不能使用的,不要认为Java中任何情况下都存在诸如整型变量默认为0,布尔型变量默认为false等这样的默认值。

1.2 操作数栈

  • 操作数栈(Operand Stack)也常称为操作栈,它是一个后人先出(Last In Firstout,LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候写人到code属性的max_stacks数据项中。在方法的执行过程中,会有各种字节码指令(存储在程序计数器中)往操作数栈中写人和提取内容,也就是出栈/入栈操作。
  • Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的“栈”就是操作数栈。如果当前线程请求的栈深度大于虚拟机所允许的最大深度,将抛出StackOverflowError异常。
  • 在概念模型中,两个栈帧是相互独立的。但是大多数虚拟机的实现都会进行优化,令两个栈帧出现一部分重叠。令下面的部分操作数栈与上面的局部变量表重叠在一块,这样在方法调用的时候可以共用一部分数据,无需进行额外的参数复制传递。

1.3 动态连接

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

以上面两行代码为例,解释一下动态连接:math.compute()调用时compute()叫符号,需要通过compute()这个符号去到常量池中去找到对应方法的符号引用,运行时将通过符号引用找到方法的字节码指令的内存地址。

1.4 方法的返回地址

  • 方法开始执行后有两种方式可以退出这个方法。第一种方式是正常完成出口(Normal Method InvocationCompletion),执行引擎遇任意一个方法返回的字节码指令,这时候可能会有返回值传递给上层的方法调用者。
  • 另外一种退出方式是异常完成出口(Abrupt Method Invocation
    Completion),在方法执行过程中遇到了异常,并且这个异常没有在方法体内得到处理,就会导致方法退出。这时没有返回值传递给上层的方法调用者。

无论采用何种退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层方法的执行状态。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整pc计数器的值以指向方法调用指令后面的一条指令等。

1.5 栈异常

栈内存为线程私有的空间,每个线程都会创建私有的栈内存。栈空间内存设置过大,创建线程数量较多时会出现栈内存溢出StackOverflowError。同时,栈内存也决定方法调用的深度,栈内存过小则会导致方法调用的深度较小,如递归调用的次数较少。

package com.kkb.test.memory;
public class StackErrorMock { 
    private static int index = 1;
    public void call(){ 
        index++;
        call();
   }
    public static void main(String[] args) { 
        StackErrorMock mock = new StackErrorMock();
        try { 
            mock.call();
       }catch (Throwable e){ 
            System.out.println("Stack deep : "+index);
            e.printStackTrace();
       }
   }
}

运行结果:

二、本地方法栈

2.1 本地方法栈的作用

要执行的方法,可以是Java方法,也可以是其他语言的方法。执行Java方法,需要Java虚拟机栈给它提供方法执行时的所需空间。执行其他语言的方法,主要指的就是C语言的方法(也叫本地方法,使用native关键字修饰的方法),它在执行的时候,也需要执行空间,那么这个空间就是本地方法栈。

2.2 为什么需要本地方法栈?

protected native Object clone() throws CloneNotSupportedException;

clone方法,不是通过java的构造方法去创建java对象,而是直接操作JVM中的堆内存,进行对象的复制。 直接操作JVM中的堆内存,这不是Java语言能干的事情,但是还必须要有该功能。怎么办呢?通过本地方法接口,去调用C函数。

   public final native void notify();

notify和wait等api方法,都是针对线程进行操作的。线程的创建和操作,都是需要通过操作系统系统底层去操作。

以上的功能,都是Java语言能具备的,所以Java语言给自己留了一个扩展的口子,就是本地方法。下图描绘了这样一个情景,就是当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。 

本文地址:https://blog.csdn.net/yx444535180/article/details/113935835