[JVM学习之路]三、运行时数据区的内部结构详解(一)(程序计数器、虚拟机栈和本地方法栈)

运行时数据区的内部结构(程序计数器、虚拟机栈和本地方法栈)

一、运行时数据区结构

在这里插入图片描述

图源-阿里

JDK8以后,原来的方法区变成了元数据区+JIT编译产物

在这里插入图片描述

在JVM运行的时候,有些区是随着进程而存在消亡,而有些是随着线程

在图中红色的方法区和堆是随着JVM的生命而创建和消亡,也就是对应着JVM进程,是进程中所有的线程所共有的;

而图中灰色区域则是与使用的线程进行一一对应,是每个线程所私有的。

也就是说,每个线程独立享有:程序计数器,本地方法栈,虚拟机栈;而所有线程共享的是堆和堆外内存(元空间)

调优都是在元空间和堆中进行,其中95%以上调优都是在堆中进行

二、程序计数器

程序计数器也称为为PC寄存器,程序钩子(PC Register,Program Counter Register)

1.特点与作用

1.它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域

2.在jvm规范中,每个线程都有自己的程序计数器,是线程私有的,生命周期与线程一致

3.任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址,如果是native方法,则是未指定值(undefined),因为程序计数器不负责本地方法栈。

4.它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成

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

6.它是唯一一个在java虚拟机规范中没有规定任何OOM(Out Of Memery)情况的区域,而且没有垃圾回收(没有GC和OOM)

**主要作用:**用于存储指向下一条指令的地址,也就是即将要执行的代码地址

2.作用流程

在这里插入图片描述

1.程序计数器存储指令地址

2.执行引擎获取程序计数器中的指令地址

3.获取到操作指令,操作局部变量表,操作数栈

4.转换成机器指令给CPU进行运算

3.常见问题

1.使用PC寄存器存储字节码指令地址有什么用呢(为什么使用PC寄存器记录当前线程的执行地址呢)

  • 多线程环境下,CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续执行
  • JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一条应该执行什么样的字节码指令

2.PC寄存器为什么会设定为线程私有?
(1)我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停滴做任务切换,这样必然会导致经常中断或恢复,如何保证分毫无差呢?

(2)为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是**为每一个线程都分配一个PC寄存器,**这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

三、虚拟机栈

1.虚拟机栈概述

基于跨平台设计,Java的指令都是根据栈设计,由于不同平台CPU不同,所以不能设计为寄存器。

优点是跨平台,指令集小编译器容易实现;缺点性能下降,实现同样的功能需要更多指令

栈是运行时的单位,堆是存储的单位

1.什么是Java虚拟机栈?

Java虚拟机栈是在每个线程创建时都会创建的,内部保存一个个栈帧,对应一次次的方法调用,是每个线程所私有的

2.生命周期

与线程一致

3.作用

主管Java程序的运行,保存方法的局部变量(8中基本数据类型,对象的引用地址),部分结果;参与方法的调用和返回

4.优点

1.是一种快速有效的分配存储方式,访问速度仅次于程序计数器

2.JVM对Java虚拟机栈只有两个操作:伴随方法执行的进栈;执行完方法后出栈。

3.不存在垃圾回收问题

不需要GC

5.面试题

开发中遇到的异常?

Java虚拟机规范允许Java站的大小是动态的或者是固定不变的。如果固定大小,线程请求分配的栈容量超过规范,就会出StackOverFlow的异常;如果可动态扩展,但是在申请内存的时候无法获得足够的内存,或者创建新线程没有足够内存去创建对应的虚拟机栈,就会抛出OutOfMemoryError

6.设置栈大小

-Xss XXkb/Mb

2.栈的存储单位

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

1.栈运行原理

  • JVM对Java栈的操作只有压栈和出栈,遵循FILO原则

  • 在一个活动线程中,在某个时间点上,只有一个活动栈帧,只有当前在执行的方法的栈帧是有效的,被称为当前栈帧,对应的方法就是当前方法,定义这个方法的类叫做当前类

  • 执行引擎运行的所有字节码指令只对当前栈帧有效

  • 如果在该方法中调用其他方法,对应新的栈帧会被创建然后放在栈的顶部,成为新的当前帧

  • 不同线程中包含的栈帧是不允许相互引用的

  • 如果当前方法调用其他方法,在返回之际会将方法执行结果给前一个栈帧,之后丢弃当前栈,使得前一个栈重新成为当前栈帧

  • Java方法有两种返回函数方式,一种是正常的函数返回,使用return指令,另一种是抛出异常。不管哪种方式都会导致栈被弹出

2.栈帧的内部结构

在这里插入图片描述

栈帧内部结构有:

1.局部变量表(Local Variables)

2.操作数栈(Operand Stack)(或表达式栈)

3.动态链接(Dynamic Linking)(或执行"运行时常量池"的方法引用)----深入理解Java多态特性必读!!

4.方法返回地址(Return Adress)(或方法正常退出或者异常退出的定义)

5.一些附加信息

后面三个也叫做帧数据区

3.局部变量表

1.特征

1.局部变量表也被称之为局部变量数组或本地变量表

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

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

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

**5.方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。**对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间。

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

2.Slot

局部变量表最基本的存储单元是Slot(变量槽)

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

this变量不存在于静态方法的局部变量表中,在构造方法和实例方法中,在Index=0处会初始化this

Slot存在重复占用,如果在方法还没执行完一个局部变量已经挂了,但已经开辟的空间不会浪费,下一个定义的局部变量就可以使用他开辟的空间

3.静态变量与局部变量对比及小结

变量的分类:

  • 按照数据类型分:

    1.基本数据类型

    2.引用数据类型

  • 按照在类中声明的位置分:

    1.成员变量:在使用前,都经历过默认初始化赋值

    • 类变量(static修饰):link的prepare阶段进行初始化——>初始化阶段initialization给类变量显式赋值即静态代码块赋值;
    • 实例变量(不被static修饰):随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值

    2.局部变量:在使用前,必须要进行显式赋值,否则,编译不通过

4.与调优关系

  • 在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收

4.操作数栈

也是栈帧中的重要结构

1.概念

每一个独立的栈帧中除了包含局部变量表外,还包含一个后进先出的操作数栈,也称为表达式栈。底层是用数组实现,编译时候已经确定长度

在方法执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈/出栈

5.栈顶缓存技术

由于栈式架构,以为着完成操作需要更多的入栈出栈指令,需要更多的读写次数。HotSpot JVM设计者提出将栈顶元素全部缓存在物理CPU的寄存器中,降低对内存的读写次数,提升执行引擎的执行效率

6.动态链接

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

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

在这里插入图片描述

编译后,存在方法区的运行时常量池保存各种常量的引用符号,然后通过指令来进行引用

在这里插入图片描述

在这里插入图片描述

这样可避免每次都得创建常量,不同栈帧引用也可以直接在运行时常量池进行引用

7.方法的调用

1.相关概念介绍

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

  • 静态链接
    当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知,且运行期保持不变时。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。
  • 动态链接
    如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。

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

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

方法的调用就分为虚方法和非虚方法

非虚方法:

  • 如果方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的。这样的方法称为非虚方法
  • 静态方法、私有方法、final方法、实例构造器(实例已经确定,this()表示本类的构造器)、父类方法(super调用)都是非虚方法

其他所有体现多态特性的方法称为虚方法

2.调用指令区分虚方法、非虚方法

普通调用指令:
1.invokestatic:调用静态方法,解析阶段确定唯一方法版本;
2.invokespecial:调用方法、私有及父类方法,解析阶段确定唯一方法版本;
3.invokevirtual调用所有虚方法;
4.invokeinterface:调用接口方法;
动态调用指令(Java7新增):
5.invokedynamic:动态解析出需要调用的方法,然后执行 .
前四条指令固化在虚拟机内部,方法的调用执行不可人为干预,而invokedynamic指令则支持由用户确定方法版本。

其中invokestatic指令和invokespecial指令调用的方法称为非虚方法

其中invokevirtualfinal修饰的除外,JVM会把final方法调用也归为invokevirtual指令,但要注意final方法调用不是虚方法),invokeinterface指令调用的方法称为虚方法。

在这里插入图片描述

invokedynamic指令是Java8以后Lambda表达式出现后就有了直接生成该指令

Java是静态语言,在新增lambda表达式后具有了动态的性质

虚方法表

  • 在面向对象编程中,会很频繁期使用到动态分派,如果在每次动态分派的过程中都要重新在累的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高性能,jvm采用在类的方法区建立一个虚方法表(virtual method table)(非虚方法不会出现在表中)来实现。使用索引表来代替查找。
  • 每个类中都有一个虚方法表,表中存放着各个方法的实际入口。
  • 那么虚方法表什么时候被创建? 虚方法表会在类加载的链接阶段被创建 并开始初始化,类的变量初始值准备完成之后,jvm会把该类的虚方法表也初始化完毕。

也就是说,每个方法对应他自己的实际入口,无序去向上层层调用,在一开始创建就已经规划好了谁调用方法

8.方法返回地址

方法返回地址是结构,存放的是调用该方法的PC寄存器的值

一个方法的结束,有两种方式:

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

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

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

正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值。

也就是说,方法返回地址保存的是执行完调用方法后,能出口继续执行调用者方法的地址

四、虚拟机栈相关面试题

  • 举例栈溢出的情况?

    -Xss设置了栈的大小,如果超过设置就会出现StackOverflowError

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

    不能,比如有些迭代次数固定,调大可能不出现,但是如果有一些无限循环的,就一定会溢出

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

    不是,会使得其他结构分配到的内存变少,影响性能

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

    不会

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

    具体问题具体分析

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

    如果说是内部产生内部消亡,就是线程安全,如果是内部产生作为返回值返回,或者是传递参数,都是不安全的

五、本地方法栈

Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用

1.特点

  • 本地方法栈也是线程私有的
  • 允许被实现成固定或者是可动态扩展的内存大小(异常会出现StackOverflowError和OutOfMemoryError,情况和虚拟机栈是一样的)
  • 本地方法是C语言实现

2、调用

如果需要使用本地方法,就将其压入栈中,使用执行引擎去执行

当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限

本地方法实际权限:

1.本地方法可以通过本地方法接口来 访问虚拟机内部的运行时数据区

2.它甚至可以直接使用本地处理器中的寄存器

3.直接从本地内存的堆中分配任意数量的内存

注意:并不是所有的JVM都支持本地方法。在HotSpot JVM中有但其他不一定有,同时它直接将本地方法栈和虚拟机栈合二为一

猜你喜欢

转载自blog.csdn.net/qq_41522089/article/details/107596640