Java虚拟机栈详解

前言

虚拟机栈也称为Java栈,每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)

栈特点基本介绍

  1. Java虚拟机栈属于线程私有,它的生命周期与线程相同(随线程而生,随线程而灭)
  2. 虚拟机栈说明了线程运行时的瞬时状态
  3. 每次方法调用,都会产生对应的栈帧
  4. 栈帧包括局部变量表、操作数栈、动态链接、方法返回地址和一些附加信息
  5. 每个方法被调用至执行完毕的过程,就对应这个栈帧在虚拟机栈中从入栈到出栈的完整过程
  6. 栈的深度有限制

在这里插入图片描述
局部变量表

  • 局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量,并在Java编译为Class文件时,就已确定该方法所需分配的局部变量表的最大容量
  • 局部变量表存放了编译期可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)「String是引用类型」,对象引用(reference类型) 和 returnAddress类型(它指向了一条字节码指令的地址)
  • 线程私有,不允许跨线程访问,随方法调用创建,方法退出销毁
  • 编译期间长度已经确定,局部变量元数据存储在字节码中
  • 局部变量表是栈帧最主要的存储空间,决定了栈的深度

操作数栈

保存中间计算的临时结果,字节码指令在执行过程中的中间计算结果存储在操作数栈

  • 操作数栈(Operand Stack)也常被成为操作栈,是一个后入先出栈,用于保存计算过程中的中间结果,同时作为计算过程中变量临时的存储空间。其最大深度在编译时就被写到了Code属性的max_stacks中
  • 操作数栈在方法的执行过程中,根据字节码指令往栈中写入数据或提取数据,即入栈和出栈操作。虽然栈是用数组实现的,但根据栈的特性,对栈中数据访问不能通过索引,而是只能通过标准的入栈和出栈操作来完成一次数据访问

动态连接

将符号引用转为直接引用

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

方法出口

存放调用方法的程序计数器

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

  • 方法返回指令 : 执行引擎遇到一个方法返回的字节码指令(return),这时候可能会有返回值传递给上层的方法调用者,这种退出方式称为正常完成出口
  • 异常退出 : 在方法执行过程中遇到了异常,并且没有处理这个异常,就会导致方法退出
  • 一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值,而方法异常退出时,返回地址要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息

假如有下面这样一段代码,在一个main方法中调用了method1,再在method1调用method2

案例演示

public class MyMethod {

    public static void main(String[] args) {
        new MyMethod().method1();
    }

    public void method1(){
        System.out.println("进入method1 ...");
        int a=1;
        int b=2;
        method2();
        System.out.println("退出method1");
    }

    public void method2(){
        System.out.println("进入method2 ...");
        int c=3;
        int d=4;
        System.out.println("退出method2");
    }

}

对应一个简单的栈帧图示可表示如下:
在这里插入图片描述

在数据结构中,栈属于一种典型的先进后出的数据结构,即先被压入栈的数据最后出来,对应到上面的案例中,Method2最后入栈,因此当Method2方法执行完毕后,最先从栈弹出,直到Main方法完成,当前这个线程的虚拟机栈就销毁,这也符合预期的方法调用返回结果

在idea中通过安装jclasslib插件,可以清楚的看到方法的字节码运行过程中的详细信息,如下,我们分析method2这个方法在调用时的栈帧过程时候,通过显示的字节码信息可以看出来该方法中的局部变量信息,结果返回时机等

在这里插入图片描述
栈空间设置

  • Java1.5之后默认每个栈空间大小为1MB
  • Java启动参数: -Xss 数值【k|m|g】
  • 栈内存分配决定了栈的最大深度,栈内存的深度在实际中是动态的,会随着每个栈中调用方法的数量不同而不同,在某些极端情况下,比如栈的空间不够了或者打满了,就会抱出栈溢出的错误,即OutOfMemoryError异常;

栈的两种常用空间配置:

  • 固定长度(推荐):达到上限时,StackOverFlowError
  • 动态扩展,当可用内存不足时,OutOfMemoryError(OOM)
  • 实际开发中,尽量不使用动态扩展的方式设置,否则因为某个线程的OOM导致整个服务器资源被耗尽而拖垮其他的服务或者功能(本人遇到过类似的生产问题),简而言之,动态扩展增大了资源调配的控制难度

StackOverFlowError案例演示

public class StackOverFlowError {

    private static long count = 0;

    public static void main(String[] args) {
        test();
    }

    public static void test(){
        count++;
        System.out.println("正在进行第:" + count + "次调用");
        test();
    }

}

这是一段自我调用最后造成死循环的调用,最后一定会由于栈内存不足报栈溢出的错误,但是设置不同的栈空间大小,这个count的次数理论上会不一样

以默认的栈空间大小
在这里插入图片描述
调大栈内存

通过启动参数配置之后再次运行,可以承受的最大调用次数明显增大了(即栈的深度增大了)
在这里插入图片描述
在这里插入图片描述

native本地方法栈

我们知道,Java属于上层语言,在对操作系统的控制层面上相比c,c++逊色不少,但是为了实现某些功能需要调用操作系统提供的相关函数,而Java中的native方法的职责就为这一功能的实现提供了一个类似转换的接口

在native方法中,只提供了接口,没有具体的实现,实现部分由C或C++去实现,总结来说:

  • 一个native方法就是一个Java调用非Java代码的接口
  • 定义一个native方法时,并不提供具体的实现
  • native的方法可以调用其他语言接口实现对操作系统更加底层的操作,比如windows下对dll文件的调用

猜你喜欢

转载自blog.csdn.net/zhangcongyi420/article/details/113062136