JVM学习笔记——虚拟机栈

虚拟机栈

背景

因为跨平台性的设计,JAVA的指令都是根据栈来设计的。
优势:跨平台,指令集小,编译器容易实现。
缺点:性能降低,实现一样的功能须要更多指令。java

栈和堆

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

简介

每一个线程在建立的时候都会建立一个虚拟机栈,其内部保存一个个的栈帧,对应着一次次的Java方法调用。
虚拟机栈主管Java程序的运行,它保存方法的局部变量,部分结果并参与方法的调用和返回。缓存

特色
  • 栈是一种快速有效的分配储存方式,访问速度仅次于程序计数器。
  • JVM直接对虚拟机栈只有两个操做。每一个方法执行,伴随着进栈,方法执行结束伴随着出栈。
  • 对于栈来讲不存在垃圾回收问题,由于只有出栈入栈两个操做。
  • JVM规范容许虚拟机栈的大小是动态的或是固定不变的。
    1.若是采用固定不变的,若是线程请求分配的栈容量超过了JVM容许的最大容量,JVM则会抛出一个StackOverflowError异常。
    2.若是采用动态扩展,在线程请求分配的站容量超过容许的最大容量,虚拟机栈会尝试向JVM申请内存,但在申请时没法得到足够的内存,或者在建立新线程时没有足够的内存去建立对应的虚拟机栈,JVM则会抛出一个OutOfMemoryError异常。
设置栈内存大小

咱们可使用参数-Xss来设置虚拟机栈的大小。
IDEA能够在run -> Edit Configurations进行设置。
Xss.png安全

虚拟机栈的存储单位

栈帧:
  • 每一个线程都有本身的栈,栈中的数据都是以栈帧的格式存在的。
  • 在这个线程上正在执行的每一个方法都各自对应一个栈帧。
  • 栈帧是一个内存区块,是一个数据集。
栈帧运行原理:
  • JVM对虚拟机栈的操做只有入栈和出栈两个操做。
  • 在一条活动的线程中,一个时间点只有一个活动的栈帧,只有当前正在执行方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧,与其对应的方法被称为当前方法,定义这个方法的类被称为当前类。
  • 执行引擎运行的全部字节码指令只针对当前栈帧进行操做。
  • 若是在当前方法中调用了新方法,对应的新栈帧会建立出来并入栈,称为新的当前栈帧。
  • 不一样线程所包含的栈是不容许互相引用的。
  • 当前方法调用了其余方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,而后当前栈帧出栈,前一个栈帧称为当前栈帧。
  • JAVA有两种返回函数的方式,一种是正常的return指令返回。另外一种是抛出未处理的异常。不论是哪一种返回方式都会致使栈帧出栈。
入栈出栈测试代码

public class StackFrameTest {架构

public static void main(String[] args) {
    StackFrameTest test = new StackFrameTest();
    test.methon1();
}

private void methon1() {
    System.out.println("方法一开始执行");
    methon2();
    System.out.println("方法一执行结束");
}
private void methon2() {
    System.out.println("方法二开始执行");
    methon3();
    System.out.println("方法二执行结束");
}
private void methon3() {
    System.out.println("方法三开始执行");
    System.out.println("方法三执行结束");
    }
}
  • 测试代码中,每出现方法调用,都会有新栈帧入栈,直到当前方法执行结束以后,原方法从新称为当前方法后继续执行。在IDEA中的DEBUG的Frames窗口中,也形象的显示了方法入栈的操做。后附图。
  • 代码中方法若是出现异常,且当前方法中没有处理,则会抛给前一个栈帧,若是前一个栈帧也没有处理则继续抛出,直到main函数,若仍未处理,则程序异常中止,由控制台打印异常信息。

栈debug.png

栈帧的内部结构

每一个栈帧中存储着如下内容:函数

  • 局部变量表
  • 操做数栈
  • 动态连接
  • 方法返回地址
  • 一些其余的附加信息

局部变量表和操做数栈主要影响着栈帧的大小,栈帧的大小影响着虚拟机栈能存放多少栈帧。性能

局部变量表

局部变量表也被称为局部变量数组或本地变量表。测试

  • 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。这些数据类型包括基本数据类型,对象引用和returnAdress类型。
  • 局部变量表线程私有,不存在数据安全问题。
  • 局部变量表所需的容量大小是在编译期就肯定下来的。
  • 在栈帧中,与性能调优关系最密切的就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
  • 局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。
局部变量表测试代码

public class LocalVariablesTest {this

public static void main(String[] args) {
    LocalVariablesTest test = new LocalVariablesTest();
    int i = 10;
    }
}

javap反编译后查看局部变量表,结果如图:
局部变量表.png
其中,spa

  • Start表示在执行指令该变量开始生效的指令行号。
  • Length表示该变量生效的行数。
  • Slot表示该变量的索引。
  • Name表示该变量的名称。
  • Signature表示该变量的类型。
  • [:表示是个数组。L表示是引用变量。I表示是int类型。

除了javap指令,咱们也能够经过JClasslib插件查看局部变量表。
jclasslib.png
局部变量表内容同上,start和length两个属性决定变量的做用域,变量在start标志的下一行开始生效。

slot
  • 局部变量表,最基本的单位是Slot(变量槽)
  • 在局部变量表中,32位之内的类型只占一个slot,64位的类型占两个slot。
  • JVM会为局部变量表中的每个slot分配一个访问索引,经过这个索引访问指定的额局部变量值。
  • 当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量会按照顺序复制到局部变量表中的每个slot上。
  • 若是须要访问一个64位的局部变量值,只须要使用前一个索引便可。例:一个long类型的变量占据了4,5两个slot,咱们使用4这个索引即可以找到这个变量。一样的,咱们须要6这个索引来找到这个long类型以后的那个变量,而不是使用5。
  • 若是当前帧是由构造方法或者实例方法建立的,那么该对象的引用变量this会存放在索引为0的slot处。

类方法为何不能使用this关键字?
由于在类方法的局部变量表中不存在this变量。

slot的重复利用

栈帧中的局部变量表中的slot是能够重用的,若是一个局部变量过了其做用域,那么在其做用域后申请的新局部变量就颇有可能复用这个过时变量的槽位。测试代码:

public void test() {
        int a = 1;
        {
            int b = 0;
            b = a + 1;
        }
        int c = a + 1;
    }

测试代码的局部变量图以下:
slot复用.png
经过索引,咱们能够看出c复用了b的slot。

类变量和局部变量的对比

变量的分类:
根据数据类型分类:

  • 基本数据类型
  • 引用数据类型

根据在类中声明的位置分类:

  • 成员变量

    • 类变量:在类加载的连接准备阶段已经给类变量赋初值,并在初始化阶段显式赋值。
    • 实例变量:随着对象的建立,在堆中分配实例变量空间,并进行默认赋值。
  • 局部变量:在使用前必须显式赋值!不然编译不经过。

操做数栈

  • 使用数组实现,在方法执行过程当中,根据字节码指令执行,往栈中写入数据或提取数据。
  • 主要用于保存计算过程的中间结果,同时做为计算过程当中变量临时的存储空间。
  • 操做数栈在一个方法刚开始执行的时候便建立出来了,这个方法的操做数栈是空的。
  • 操做数栈有一个明确的栈深度用于储存数值,最大深度在编译器就肯定好了。
  • 操做数栈并不是采用索引的方式来进行数据访问,而只能经过入栈和出栈操做完成一次数据访问。
测试代码
public void testadd() {
       byte i = 15;
       int j = 8;
       int k = i + j;
    }

反编译后的字节码指令以下:

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

字节码指令解析:
0:将数值15压入操做数栈。
2: 将操做数栈的当前栈帧压入局部变量表,索引为1。
3:将数值8压入操做数栈。
5:将操做数栈的当前栈帧压入局部变量表,索引为2。
6:读取局部变量表中索引为1的变量进操做数栈。
7:读取局部变量表中索引为2的变量进操做数栈。
8:执行加操做,并将结果压入操做数栈。
9:将操做数栈的当前栈帧压入局部变量表,索引为3。
10:方法正常返回结束。

栈顶缓存技术
  • 基于栈式架构的虚拟机完成一项操做的时候须要使用更多的入栈和出栈操做,会出现更多的读写操做。
  • 将栈顶元素所有缓存到物理的寄存器中,下降对内存的读写次数,提高执行引擎的执行效率。

动态连接

有些地方可能会将方法返回地址,动态连接和附加信息称为帧数据区。

  • 每个栈帧内部都包含一个指向运行时常量池中该站真所述方法的引用,包含这个引用的目的就是为了支持当前方法的代码可以实现动态连接。
  • 在java源文件被编译成字节码文件时,全部变量和方法引用都做为符号引用保存在class文件的常量池里。动态连接的做用就是为了将这些符号引用转换为直接引用。
测试代码
public class DynamicLinkingTest {

    int num = 1;
    
    public void methonA() {
        System.out.println("methonA");
    }
    
    public void methonB() {
        System.out.println("methonB");
        methonA();
        num++;
    }
}

反编译以后的执行指令以下:

Code:
      stack=3, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #6                  // String methonB
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: aload_0
         9: invokevirtual #7                  // Method methonA:()V
        12: aload_0
        13: dup
        14: getfield      #2                  // Field num:I
        17: iconst_1
        18: iadd
        19: putfield      #2                  // Field num:I
        22: return

咱们能够看到在指令后出现大量的#开头的符号引用,这些引用能够在编译后的常量池中找到对应的实际方法,测试代码编译后的常量池以下图:
常量池.png

为何要有运行时常量池?

为了提供一些符号和常量,便于指令的识别,减小内存,提升复用。

方法的调用

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

  • 静态连接:当一个字节码文件被装载进JVM时,若是被调用的目标方法在编译器可知,且运行期保持不变,这种称为静态连接。
  • 动态连接:若是被调用的方法在编译器没法肯定,只有在程序运行期才能将其转换为直接引用,称为动态连接。

根据两种连接对应的绑定机制,绑定是指一个字段,方法或者类在符号引用被替换成直接引用的过程,这仅仅发生一次。

  • 早期绑定:早期绑定指被调用的目标若是在编译器可知,且运行期保持不变。
  • 晚期绑定:被调用的目标只能在运行时才能从符号绑定转为直接绑定,这称为晚期绑定。
例子
  • 实例方法就是典型的晚期绑定,在程序运行以前,没法肯定将要调用哪一个实例的方法。
  • 在子类构造器中显式调用super()则是早期绑定的例子,在编译期间,就能够肯定该方法的直接引用。
虚方法和非虚方法

非虚方法:若是方法在编译期就肯定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法。如静态方法,私有方法,final方法,实例构造器,父类方法都是非虚方法。其余方法都称为虚方法。
虚拟机中提供了一下几条方法调用指令:

  • 普通调用指令

    • invokestatic 调用静态方法,解析阶段肯定惟一方法版本。
    • invokespecial 调用<init>方法,私有及父类方法,解析阶段肯定惟一方法版本。
    • invokevirtual 调用虚方法。
    • invokeinterface 调用接口方法。
  • 动态调用指令

    • invokedynamic 动态解析出须要调用的方法,而后执行。

前四条指令方法的调用执行不可人为干预,而动态调用支持由用户肯定方法版本。其中invokestatic和invokespecial指令调用的方法称为非虚方法,其他的(final修饰的除外)称为虚方法。
没有显式使用super的方法调用也会被认为是虚方法,使用invokevirtual调用。

invokedynamic指令

JDK7中为了实现动态类型语言支持而作出的改进,出现了invokedynamic指令。但直到JDK8的Lambda表达式的出现,invokedynamic指令的生成,在JAVA种才有了直接的生成方式。

静态类型语言和动态类型语言

这两种语言最重要的区别在于对类型的检查是在编译期间仍是在运行期间,前者为静态类型语言,后者为动态类型语言。直白的说,静态语言判断变量的类型,动态语言判断变量值的类型。

方法重写

方法重写的本质

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

IllegalAccessError异常介绍:
程序试图访问或修改一个属性或调用一个方法,这个属性或方法你没有权限访问。通常来讲这个会引发编译器异常,若是发生在运行期间,就说明一个类发生了不兼容的改变。

虚方法表

为了提升性能,JVM在方法区创建一个虚方法表来实现,使用索引表来代替查找。
虚方法表主要在类加载的连接解析阶段建立。

方法返回地址

存放调用该方法的程序计数器的值。在方法结束以后都须要返回到该方法被调用的位置,方法正常退出时,调用者的程序计数器的值做为返回地址,即调用该方法指令的下一条指令的地址。而经过异常退出须要经过异常表肯定,栈帧中不保存这部分信息。
因此,经过异常完成的方法退出不会给它的上层调用者任何的返回值。
补充:方法返回指令!

  • ireturn负责返回值是booleanbytecharshortint
  • lreturn负责返回值是long
  • freturn负责返回值是float
  • dreturn负责返回值是double
  • areturn负责返回值是引用类型
  • return负责生命为void的方法,实例初始化方法,类和接口的初始化方法。

栈帧中的附加信息

这个部分可能会有,也可能没有,主要看虚拟机的具体实现。

相关文章
相关标签/搜索