JVM学习之-运行时数据区(Runtime data area)

Posted TyuIn

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM学习之-运行时数据区(Runtime data area)相关的知识,希望对你有一定的参考价值。

   一、运行时数据区的认识

线程共享和线程私有的区域

名词解释:PC程序计数器    VMS 虚拟机栈  NMS 本地方法栈 

 从上图可以大致的了解到线程私有区域的构成,接下来就对里面的每一个部分展开描述一下:

(1)程序计数器(PC)

        内存空间小,线程私有。字节码解释器工作是就是通过改变这个计数器的值来选取下一条需要执行指令的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器完成。

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

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

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

(2)虚拟机栈(VM Stack)

        描述的是 Java 方法执行的内存模型:每个方法在执行时都会床创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

虚拟机栈中可能出现的异常:

  • StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
  • OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。

1、栈帧(Stack Frame)

        栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

        在编译程序代码的时候,栈帧中需要多大的局部变量表内存,多深的操作数栈都已经完全确定了。因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

        如上图所示,当前栈桢总是指向栈顶【在活动线程中,只有位于栈顶的栈帧才是有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法。】执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。

2、局部变量表(Local Variable Table)

        存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,它不等同于对象本身,根据不同的虚拟机实现,它可能是一个指向对象起始地址的引用指针,也可能指向一个代表对象的句柄或者其他与此对象相关的位置)和returnAddress 类型(指向了一条字节码指令的地址)。其中64 位长度的long 和double 类型的数据会占用2 个局部变量空间(槽 Slot),其余的数据类型只占用1 个槽。【从这里也反映了一个槽的大小是4字节】局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题。【这当然可以说是废话】

【需要了解什么是编译期和运行期的可以看一下下面这篇博客:Java 编译期与运行期,别傻傻分不清楚! - Java技术栈 - 博客园

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

最后提一些局部变量表中的拓展知识:

 1、Slot(槽)复用

        为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,也就是说当PC计数器的指令指向已经超出了某个变量的作用域(执行完毕),那这个变量对应的Slot就可以交给其他变量使用。【如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。

  • 优点 : 节省栈帧空间。 
  • 缺点 : 影响到系统的垃圾收集行为。(如大方法占用较多的Slot,执行完该方法的作用域后没有对Slot赋值或者清空设置null值,垃圾回收器便不能及时的回收该内存。) ->  注意:垃圾回收是在堆和方法区的,栈中没有垃圾回收
public class A {

    public static void main(String[] args) {
        int a = 0;
        {
            int b = 0;
            b = a + 1;
        }
        // 变量c使用之前已经销毁的变量b的slot
        int c = a + 1;
    }
}

 2.扩展

        如果当前栈帧是由构造方法或者实例方法创建的,那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序继续排列。如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或double类型变量)【下面是一段简单的测试代码】

public class A {

    public void test(){
        int a = 10;
        String b = "hello";
    }

}

3、动态链接(Dynamic Linking)

        每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking)。在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在每一次运行时期转化为直接引用。这部分称为动态连接。【比如: invokedynamic指令。】

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

4、方法返回地址(Return Address)

         方法返回地址(Return Address)(方法正常退出或者异常退出的定义)---- 存放调用该方法的pc寄存器的值。

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

  • ​ 方法返回指令 (return): 执行引擎遇到一个方法返回的字节码指令,这时候有可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口。
  • 异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出。

        一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

5、操作数栈(Operand Stack):(表达式栈)

        操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始执行的时候,一个新的栈帧也会随之被创建出来,这时这个方法的操作数栈是空的。      

         某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈。比如:执行复制、交换、求和等操作。

    【下面是一个简单的代码讲解以及字节码的具体分析】

public class A {
    /*
         0 bipush 15   操作数 byte 15 入栈
         2 istore_1    操作数出栈,局部变量表索引为 1的位置存储 int 15
         3 bipush 8    操作数入栈 byte 8
         5 istore_2    操作数出栈,局部变量表索引为 2的位置存储 int 8
         6 iload_1     从局部变量表对应索引处获取值,入栈
         7 iload_2     从局部变量表对应索引处获取值,入栈
         8 iadd        15 和 8 出栈求和,再入栈
         9 istore_3    将计算结果存储再局部变量表索引为 3位置 int 23
        10 sipush 800
        13 istore 4
        15 return
     */
    public static void main(String[] args) {
        byte i = 15;
        int j = 8;
        int k = i + j;

        int m = 800;
    }
}

 简单的拓展问题

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

如果只有一个线程可以操作此数据,则必是线程安全的。如果有多个线程操作此数据,则此数据是共享数据。如果不考虑同步机制的话,会存在线程安全问题。

2、分配的栈内存越大越好么?

不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个虚拟机的内存空间是有限的。

(3)本地方法栈(Native Method Stack)

        本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java 方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native (本地)方法服务。虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定,因此具体的虚拟机可以自由实现它。甚至有的虚拟机(譬如Sun HotSpot 虚拟机)直接就把本地方法栈和虚拟机栈合二为一。与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowError 和OutOfMemoryError异常。

         【方法区与堆内存在下一篇博客再进行讲解。】本篇主要记录博主在学习java虚拟机过程的笔记以及自己的总结,内容及配图来源《深入理解java虚拟机(第三版)-JVM高级特性与最佳实践 》,书的作者是:周志明老师。文章中的图片主要是尚硅谷JVM课程学习的图片,有兴趣的可以去 blibli 搜索学习。

 

以上是关于JVM学习之-运行时数据区(Runtime data area)的主要内容,如果未能解决你的问题,请参考以下文章

JVM03_运行时数据区概述

JVM运行时数据区篇(堆空间基本概述)

醒酒菜:动画图解核心内存区--堆

#yyds干货盘点# 醒酒菜:动画图解核心内存区--堆

JVM内存结构和常量池

Java学习之二(线程(了解) JVM GC 垃圾回收)