JVM之虚拟机栈详细讲解

2023-05-16

Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。

如下图所示,其中灰色部分为单个线程私有的,红色部分是多个线程所共享的。

  • 每个线程:包括程序计数器、栈、本地方法栈
  • 线程间共享:堆、堆外内存(永久代或元空间、代码缓存)

程序计数器

程序计数器(Program Counter Register)是一块较小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器。在Java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

由于Java虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何OutOfMemoryError情况的区域。

虚拟机栈

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的方法调用。

开发者遇到的异常

Java虚拟机规范允许Java栈的大小是动态的或者固定不变的。

如果采用固定大小的Java虚拟机栈,那么每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个StackOverflowError异常。

如果采用动态扩展的Java虚拟机栈,当在尝试动态扩展时无法申请到足够的内存时,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈时,那么Java虚拟机会抛出一个OutOfMemoryError异常。

栈的存储单位

每个线程都有自己的栈,栈中的数据都是以栈帧的格式存在的。在这个线程上正在执行的每个方法都各自对应一个栈帧。栈帧是一个内存区块,用于存储局部变量表、操作数栈、动态链接、方法返回地址等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

局部变量表

局部变量表:Local Variables,又称为局部变量数组或本地变量表。

定义为一个数字数组,主要是用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference)、以及returnAddress类型。

由于局部变量表是建立在线程的虚拟机栈上的,是线程的私有数据,因此不存在数据安全问题。

局部变量表所需要的容量大小是在编译期确定下来的,并保存在方法的 Code 属性 maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的。

方法嵌套调用次数是由栈的大小决定的。一般来说,栈越大,方法嵌套调用次数就越多。对于一个函数而言,它的参数和局部变量越多,使得局部变量表越大,它对应的栈帧也就越大,从而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

局部变量表中的变量只有在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成从参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也随之销毁。

关于Slot的理解

局部变量表中最基本的存储单位是Slot(变量槽),局部变量表中存放编译期可知的各种基本类型(8种),引用类型(reference)、returnAddress 类型的变量。

在局部变量表中,32位以内的类型只占用一个位置 Slot (包括 returnAddress类型),64位的类型( long 和 double)占用两个slot。

byte、short、char 在存储前被转换为 int,boolean也被转换为 int , 0 表示false,非0 表示 true。long 和 double 则占据两个 slot。

JVM会为局部变量表中的每一个Slot分配一个访问索引,通过这个索引即可成功的访问到局部变量表中指定的局部变量值。

当一个实例方法被调用时,它的方法参数和方法体内部定义的局部变量将会按照顺序被复制到局部变量表中的每一个Slot上。如果要访问局部变量表中一个64位的局部变量值时,只需要使用前一个索引即可。

如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的Slot处,其余的参数按照参数表顺序继续排列。

Slot的重复利用

栈帧中的局部变量表中的槽位是可以重用的,如果一个局部变量超过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。

public class test {
    public void local(){
        {
            int a=1;
            System.out.println(a);
        }
        //复用局部变量a的槽位
        int b=2;
    }
}

操作数栈

每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出的操作数栈,也可以称之为表达式栈。

操作数栈,在方法执行的过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈和出栈。

操作数栈,主要用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。

每一个操作数栈都有一个明确的栈深度用于存储数值,其所需的最大深度是在编译期就定义好了,保存在方法的Code属性中,为maxstack值。

栈中元素可以是任何的Java数据类型。

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

操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问。

操作数栈中的元素的数据类型必须与字节码指令的序列严格匹配,这由编译器在编译期间进行验证,同时在类加载过程中的类检验阶段的数据分析阶段要再次验证。另外我们常说,java虚拟机的解释引擎是基于栈的执行引擎,其中的栈就是指操作数栈。

下面我们来看一个案例:

public class test {

    public void local(){
        int i=15;
        int j=8;
        int k=i+j;
    }
}

我们使用javap -v test.class 进行反编译,其对应的字节码指令如下所示,我们重点关注一下local方法对应的指令。

通过观察字节码指令,bipush指令用于将15和8进行入栈操作;iadd指令用于进行加法操作,i代表的是int,所以iadd代表两个int类型的数进行相加操作。

具体的执行流程如下所示:

  1. 首先执行第一条语句,PC寄存器指向的是0 ,也就是指令的地址为0,然后使用bipush让操作数15入栈。
  2. 执行完后,让PC+1,指向下一个指令istore_1,istore_1是让操作数栈中的元素存储到局部变量表1的位置。
  3. 同理,继续向下执行,让操作数8页入栈,然后执行istore_2指令,让栈中的元素存储到局部变量表2的位置。
  4. 然后继续向下执行,执行iload_1、iload_2,将局部变量表位置1和2对应的元素加载到操作数栈中。
  5. 然后执行iadd命令,将操作数栈中两个元素执行相加操作,然后入栈。
  6. 然后执行istore_3命令,让操作数栈中的元素 23 存储到局部变量表3的位置。
  7. 最后执行return,则表示退出方法。

栈顶缓存技术

基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也意味着将需要更多的执行分派次数和内存读写次数。

由于操作系统是存储在内存中的,因此频繁地执行内存的读写必然会影响执行效率。为了解决这个问题,HotSpot VM的设计者们提出了栈顶缓存技术,也就是将栈顶的元素全部缓存在物理CPU的寄存器中,以此降低对内存的读写次数,提升执行引擎的执行效率。

动态链接

每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的是为了支持当前方法的代码能够实现动态链接(Dynamic Linking)。比如 invokedynamic 指令。

在java源文件被编译成字节码文件时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。比如:描述一个方法调用了另外的其它方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。

下面给个例子。

public class test {

    public void local() {
        int i = 15;
        int j = 8;
        int k = i + j;
    }

    public  void  local1(){
        local();
    }
}

然后通过 javap -v test.class,进行反编译,可以看到所有的变量和方法引用都作为符号引用保存在常量池中。

public class test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#20         // java/lang/Object."<init>":()V
   #2 = Methodref          #3.#21         // test.local:()V
   #3 = Class              #22            // test
   #4 = Class              #23            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               LocalVariableTable
  #10 = Utf8               this
  #11 = Utf8               Ltest;
  #12 = Utf8               local
  #13 = Utf8               i
  #14 = Utf8               I
  #15 = Utf8               j
  #16 = Utf8               k
  #17 = Utf8               local1
  #18 = Utf8               SourceFile
  #19 = Utf8               test.java
  #20 = NameAndType        #5:#6          // "<init>":()V
  #21 = NameAndType        #12:#6         // local:()V
  #22 = Utf8               test
  #23 = Utf8               java/lang/Object
{
  public test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ltest;

  public void local();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        15
         2: istore_1
         3: bipush        8
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 6
        line 12: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Ltest;
            3       8     1     i   I
            6       5     2     j   I
           10       1     3     k   I

  public void local1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #2                  // Method local:()V
         4: return
      LineNumberTable:
        line 15: 0
        line 16: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Ltest;
}

这里提个问题,你知道为什么需要运行时常量池吗?

因为在不同的方法中,都有可能调用常量、方法,所以将它们放在常量池中,只存储一份,节省了空间。

方法的调用

在JVM中,将符合引用转换为调用方法的直接引用与方法的绑定机制相关。

链接

静态链接:

当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。

动态链接:

如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种转换过程具备动态性,因此也被称为动态链接。

绑定机制

绑定是指一个字段、方法或者类在符号引用被替换为直接引用的过程。

早期绑定:

早期绑定就是指被调用的方法如果在编译期可知,且运行时保持不变,即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符合引用转换为直接引用。

晚期绑定:

如果被调用的方法在编译期无法被确定下来, 只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式被称为晚期绑定。

虚方法与非虚方法

如果方法在编译期就确定了具体的调用版本,并且这个版本在运行时是不可变的,那么就称此方法为非虚方法。

静态方法、私有方法、final 方法、实例构造器、父类方法都是非虚方法,剩下的其它方法是虚方法。

JVM虚拟机提供的几个方法调用指令。

  • invokestatic:调用静态方法,解析阶段确定方法的版本
  • invokespecial:调用方法、私有及父类方法,解析阶段确定方法的版本
  • invokevirtual:调用所有的虚方法
  • invokeinterface:调用接口方法
  • invokedynamic:动态解析出需要调用的方法,然后执行

前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而 invokedynamic 指令则支持由用户确定方法的版本。

java语言中方法重写的本质

  1. 找到操作数栈顶第一个元素所执行的对象的实际类型,记做C
  2. 如果在类型C中找到与常量中描述符和简单名称都相符的方法,则进行访问权限校验,如果通过则返回这个方法的直接引用,查找过程结束。如果不通过,则返回 java.lang.IllegalAccessError 异常。
  3. 否则,按照继承关系从下往上依次对C的各个父类进行第二步的搜索和验证过程。
  4. 如果始终没有找到合适的方法,则抛出java.lang.AbstractMethodError异常。

方法返回地址

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

  • 正常执行完成
  • 出现未处理的异常,非正常退出

无论通过哪种方式退出,在方法退出后都会返回到该方法被调用的位置。方法正常退出时,调用者的PC寄存器的值作为方法的返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定的,栈帧中一般不会保存这部分信息。

当一个方法开始执行后,只有两种方式可以退出这个方法。

  1. 执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口。
  2. 一个方法在正常调用完成之后,究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。
  3. 在字节码指令中,返回指令包含 ireturn(当返回值类型为boolean、byte、char、short和int类型时使用),lreturn(Long类型),freturn(Float类型),dreturn(Double类型),areturn。另外还有一个 return 指令声明为 void 的方法,实例初始化方法,类和接口的初始化方法使用。
  4. 在方法执行的过程中遇到异常,并且这个异常没有在方法内进行处理,也就是说只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。
  5. 方法在执行的过程中,抛出异常时的异常处理,存储在一个异常处理表中,方便在发生异常的时候找到处理异常的代码。

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

一些异常信息

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

#####一道面试题

方法中定义的局部变量是否线程安全?这个得具体问题具体分析。

如下所示:

public class test {
    //线程内部创建的,属于局部变量, 是线程安全的
    public static void m1(){
        StringBuilder s1=new StringBuilder();
        s1.append("a");
        s1.append("b");
    }
    //线程不安全的,操作的是共享数据s2
    public static void m2(StringBuilder s2){
        s2.append("a");
        s2.append("b");
    }
    //线程不安全的,因为有返回值,有可能被其它程序所调用
    public static StringBuilder m3(){
        StringBuilder s3=new StringBuilder();
        s3.append("a");
        s3.append("b");
        return s3;
    }
}

如果感觉小编写得不错,请素质三连:点赞+转发+关注。我会努力写出更好的作品分享给大家。更多JAVA进阶学习资料小编已打包好,可以关注私信找我领取哦!

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

JVM之虚拟机栈详细讲解 的相关文章

  • 如果使用了 waitFor,为什么杀死 JVM 也会终止其子进程?

    If waitFor不使用时 杀死JVM对其子进程没有影响 这是一个例子 重击脚本 usr bin env bash echo Sleeping gt log sleep 30 echo Wake up gt gt log Java代码 p
  • 多个 JVM 与单个应用程序服务器

    我正在处理一个系统 该系统为每个客户在其自己的 JVM 中运行一个 Java 应用程序 我们有大约六台专用服务器 现在总共运行着近 100 个 JVM 以及用于管理这些 JVM 的自定义脚本集 这个设置在这一点上确实显示出了它的年龄 管理如
  • 空闲后 JVM JIT 去优化

    我使用Java主要是为了编写宠物项目 这些项目大部分时间都是闲置的 闲置数小时 天后 响应时间增加到秒 最多 10 秒 然后慢慢减少到 200 300 毫秒 据我了解 发生这种情况是因为 JIT去优化 https www safariboo
  • 是否可以在 java jit 上使用 Final boolean 删除跳转?

    正如我们所知 有些人说 java JIT 比 C 更快 我有一些想法 可以利用 JIT 并在运行时代码中删除一些指令 这是我尝试过的示例代码 Created by kadirbasol on 4 6 14 public class Remo
  • 在消费者线程中使用 JNI 的生产者-消费者程序中无法捕获 SIGINT 信号

    我正在为生产者消费者问题编写一个程序 生产者生产数据并将数据推送到boost spsc queue消费者处理它 在消费者线程中 我正在使用JNI从我用 c 编写的代码中调用一些 java 函数 I am initializing and c
  • 非线性条件断点

    是否可以设置断点 使程序在指令满足某些条件后停止执行true 一个类有一个名为的变量currency我想设置一个断点 使程序在之后的任何行停止currency 20 我不知道行号在哪里currency已更改 因此这就像在其中放置一个断点每行
  • -XX:MaxPermSize 带或不带 -XX:PermSize

    我们遇到了一个Java lang OutOfMemoryError 永久代空间错误并查看 tomcat JVM 参数 除了 Xms and Xmx我们还指定了参数 XX MaxPermSize 128m 经过一些分析后 我可以看到 Perm
  • Spring批处理返回自定义进程退出代码

    我有一个包含多个作业的 jar 我只想每次只执行一个作业并检索自定义退出代码 例如 我有基本工作 retrieveErrorsJob 配置 只需一步即可读取输入 XML 文件并将数据写入特定数据库表中 应用类 SpringBootAppli
  • 如何找出一个对象有多少个引用? [复制]

    这个问题在这里已经有答案了 是否可以和 或容易地找出任意对象有多少个传入引用 也就是说 有多少对象引用它 提前致谢 简短的回答是 你自己数一下 StackOverflow 的另一个问题有一些有用的答案和资源 是否可以获得对象引用计数 htt
  • 强制小程序加载到同一个 JVM 实例中?

    我接管了一个旧 Web 应用程序的维护工作 该应用程序使用嵌入在同一网页中的多个小程序 这曾经工作得很好 但自从引入了下一代 Java 插件 1 6 0 10 后 不保证各个小程序能够加载到同一个 JVM 实例中 如果它们加载在不同的 JV
  • 为什么我的网络浏览器版本的 java 与命令行版本不同

    the Safari browser says I am running 7 But the command line says I am running 6 prg ceylon dist samples helloworld java
  • IntelliJ 调试:暂停整个虚拟机,然后进入单线程

    我正在调试一个具有大量线程的应用程序 我的断点设置为暂停整个虚拟机 当线程遇到其中一个断点时 我想使用 Step Over 但这似乎会恢复整个虚拟机 直到该步骤完成 如果我可以只单步执行到达断点的单个线程 那确实会有帮助 在 Intelli
  • 如何使用IntelliJ IDEA ThreadDumpVisualizer插件分析Java线程转储

    我正在寻找使用一些线程转储分析器来分析 Java 线程转储并安装了ThreadDumpVisualizerIntelliJ IDEA 插件 但不知道如何使用它 插件页面 https plugins jetbrains com plugin
  • 内存中的方法表示是什么?

    在思考一下 Java C 编程时 我想知道属于对象的方法如何在内存中表示 以及这一事实如何涉及多线程 是为内存中的每个对象单独实例化一个方法还是执行 同一类型的所有对象共享该方法的一个实例 如果是后者 执行线程如何知道哪个对象是 要使用的属
  • 是否可以使 java.lang.invoke.MethodHandle 与直接调用一样快?

    我正在比较性能MethodHandle invoke以及直接静态方法调用 这是静态方法 public class IntSum public static int sum int a int b return a b 这是我的基准 Stat
  • Scala 泛型 - 为什么我无法在泛型类中创建参数化对象?

    我目前正在学习scala 为什么此代码不起作用 class GenClass T var d T var elems List T Nil def dosom x T var y new T y 我得到 错误 需要类类型 但找到了 T 代替
  • 估计 64 位 Java 中最大安全 JVM 堆大小

    在分析存在一些问题的 64 位 Java 应用程序的过程中 我注意到分析器本身 YourKit 正在使用真正大量的内存 我在 YourKit 启动脚本中得到的是 JAVA HEAP LIMIT Xmx3072m XX PermSize 25
  • 如何在JVM不退出的情况下多次运行Java程序?

    假设我有一个Java程序Test class 如果我使用下面的脚本 for i in 1 10 do java Test done JVM每次都会退出java Test被调用 我想要的是跑步java Test在不退出JVM的情况下多次执行
  • Java 中的引用变量里面有什么?

    我们知道对象引用变量保存表示访问对象的方式的位 它不保存对象本身 但保存诸如指针或地址之类的东西 我正在阅读 Head First Java 第 2 版 一书 书中写道 第 3 章第 54 页 在 Java 中我们并不真正知道什么是 在引用
  • Oracle 的商业 Hotspot JVM 相对于 OpenJDK 有哪些性能优势?

    正如这个问题中所描述的 OpenJDK 与 Java HotspotVM https stackoverflow com q 44335605 1593077 Oracle 的商业 Hotspot JVM 本质上是 OpenJDK 加上一些

随机推荐