欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

荐 【探究JVM四】Java方法执行的线程内存模型——虚拟机栈 字节码指令追踪,万字长文深入探究内部结构

程序员文章站 2024-01-07 13:45:22
JVM运行时数据区重要结构,Java方法执行的线程内存模型——虚拟机栈 。本文通过字节码指令追踪,万字长文带你深入探究虚拟机栈的内部结构!...

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

1. 虚拟机栈概述

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

虚拟机栈出现背景

由于跨平台性的设计,Java指令都是基于栈(8位对齐)来设计的。不同平台的CPU架构不同,所以不能设计为基于寄存器的(16位为对齐)。

【基于栈的优势】:跨平台,指令集小,编译器容易实现;

【劣势】:性能下降,实现的同样的功能指令多

内存中的栈与堆

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

  • 虚拟机栈解决程序的运行问题,程序执行最终都是压入虚拟机栈的栈帧中

  • 堆解决的是数据存储问题,(除去基本类型之外)主体数据均是存储在堆上

就好比是我们电脑的内存和硬盘,程序的执行都需要加载到内存中来运行。但是保存数据都是持久化在硬盘上的,硬盘中的数据需要读取到内存中才能执行。

虚拟机栈的基本内容

【作用】:管理Java程序的运行,保存方法的局部变量、部分结果,参与方法的调用和返回

【生命周期】:与线程的生命周期相同

【特点】

  • 访问速度仅次于PC计数器(只涉及到入栈和出栈的操作)

  • 不存在垃圾回收问题

会存在OOM和*

出现的异常

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

《Java虚拟机规范》中规定了两类异常状况:

  • 如果线程请求的栈的深度大于虚拟机所允许的最大深度,抛出*Error

  • 如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法中请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那JVM将会抛出OutOfMemoryError异常

HotSpot虚拟机栈容量不可以动态扩展,但是申请失败是仍会产生OOM

【手动设置虚拟机栈的容量大小】

  • -Xss256k(单位为k、m、g,忽略大小写)

2. 虚拟机栈的内部结构

栈中存储什么

虚拟机栈的基本单元是栈帧(Stack Frame),一个栈帧对应一个Java方法的调用

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

【执行原理】

由于虚拟机栈是线程私有的,不同的线程有不同的栈,不同的栈之间数据不能共享。即不可能存在一个栈帧中引用另外一个线程的栈帧。它们可以通过同一个进程来共享数据。

Java方法有两种返回函数的方式:

  • 一种是正常的函数返回,使用 return指令;

  • 另外一种是抛出异常。

不管使用哪种方式,都会导致栈帧被弹出

栈帧的内部数据

每个栈帧中存储着5部分数据:

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

  • 局部变量表(Local Variables)

  • 操作数栈(Operand Stack)

  • 动态链接(Dynamic Linking)

  • 方法返回地址(Return Address)

  • 附加信息

接下来,我们分别讨论这5部分的作用


3. 局部变量表

局部变量表Local Variables(局部变量数组或者本地变量表)。

  1. 局部变量表是一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量。

    • 数据类型包括:基本数据类型、引用类型、返回值类型
  2. 线程私有数据,不存在线程安全问题

  3. 局部变量表的容量大小在编译期间被确定,在方法运行期间不会改变

使用 jclasslib插件查看out包下反编译后的字节码文件

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

main方法对应的字节码文件

 0 new #1 <iqqcode/jvmstack/LocalVariablesTest01>
 3 dup
 4 invokespecial #2 <iqqcode/jvmstack/LocalVariablesTest01.<init>>
 7 astore_1
 8 bipush 10
10 istore_2
11 aload_1
12 invokevirtual #3 <iqqcode/jvmstack/LocalVariablesTest01.test1>
15 return
package iqqcode.jvmstack;

import java.util.Date;

/**
 * @Author: Mr.Q
 * @Date: 2020-06-25 09:39
 * @Description:局部变量表容量测试
 */
public class LocalVariablesTest01 {
    private int count = 0;

    public static void main(String[] args) {
        LocalVariablesTest01 test = new LocalVariablesTest01();
        int num = 10;
        test.test1();
    }

    public static void testStatic(){
        LocalVariablesTest01 test = new LocalVariablesTest01();
        Date date = new Date();
        int count = 10;
        System.out.println(count);
        //因为this变量不存在于当前方法的局部变量表中!!
        //System.out.println(this.count);
    }

    //关于Slot的使用的理解
    public LocalVariablesTest01(){
        this.count = 1;
    }

    public void test1() {
        Date date = new Date();
        String name1 = "iqqcode";
        test2(date, name1);
        System.out.println(date + name1);
    }

    public String test2(Date dateP, String name2) {
        dateP = null;
        name2 = "Mr.Q";
        double weight = 130.5;//占据两个slot
        char gender = '男';
        return dateP + name2;
    }

    public void test3() {
        this.count++;
    }

    public void test4() {
        int a = 0;
        {
            int b = 0;
            b = a + 1;
        }
        //变量c使用之前已经销毁的变量b占据的slot的位置
        int c = a + 1;
    }

    public void test5Temp() {
        int num;
        //System.out.println(num);//错误信息:变量num未进行初始化
    }
}

槽Slot:

数字数组中存放法的数据类型包括:基本数据类型(boolean、byte、char、short、int、float、long、double)、引用类型(reference引用指针类型)、返回值类型(returnAddress指向字节码指令的地址)

局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。

局部变量表和操作数栈底层都是采用数组实现的,所以一旦初始化后,长度是固定不变的

这些数据类型在局部变量表中的存储空间用局部变量槽(Slot)来表示,longdouble是64位的占两个槽,其余占一个。

所以局部变量表的容量就是槽的个数,在编译期间就是确定的

变量的分类:

一、按照数据类型分:① 基本数据类型 ② 引用数据类型

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

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

  • 类变量: Linking的Prepare阶段:给类变量默认赋值 ====> initial阶段:给类变量显式赋值

  • 实例变量:随着对象的创建,会在堆空间中分配实例变量空间,并进行默认赋值

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


4. 操作数栈

栈帧中存储的第二部分数据----操作数栈Operand Stack(也称表达式栈)。

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

操作数栈对于数据的存储跟局部变量表是一样的,但是跟局部变量表不同的是,操作数栈对于数据的访问不是通过下标,而是通过标准的栈操作来进行的(压入与弹出)

对于数据的计算是由CPU完成的,所以CPU在执行指令时每次会从操作数栈中弹出所需的操作数,经过计算后再压入到操作数栈顶。

另外,我们说Java虚拟机的解释引擎是基于栈的执行引擎,其中的栈指的就是操作数栈。

我们通过一个例子来进行代码追踪分析:

public class PCRegisterTest {
    public static void main(String[] args) {
        int i = 10;
        int j = 20;
        int k = i + j;
    }
}

反编译之后的主体代码:

{
  public iqqcode.pcregister.PCRegisterTest();
    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 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Liqqcode/pcregister/PCRegisterTest;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: bipush        20
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: return
      LineNumberTable:
        line 10: 0
        line 11: 3
        line 12: 6
        line 13: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  args   [Ljava/lang/String;
            3       8     1     i   I
            6       5     2     j   I
           10       1     3     k   I
}
SourceFile: "PCRegisterTest.java"

我们对代码进行跟踪分析

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

对照字节码指令,我们逐行分析:

初始情况

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

I. 根据PC计数器记录的代码偏移位置0,操作数10入栈

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

II. 根据PC计数器记录的代码偏移位置2,操作数10出栈放入到局部变量表中索引为1的位置处

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

III. 根据PC计数器记录的代码偏移位置3,操作数20入栈

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

IV. 根据PC计数器记录的代码偏移位置5,操作数20出栈放入到局部变量表中索引为2的位置处

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

V. 从局部变量表中取出索引1位置的数(10),放入到操作数栈中

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

VI. 从局部变量表中取出索引2位置的数(20),放入到操作数栈中

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

VII. 将两个操作数出栈相加后加过再入栈

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

VIII. 操作数30放入局部变量表索引为3的位置处

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

IX. 方法结束返回(返回为空)


5. 动态链接

动态链接、方法返回地址、附加信息统称为帧数据区

荐
                                                        【探究JVM四】Java方法执行的线程内存模型——虚拟机栈  字节码指令追踪,万字长文深入探究内部结构

动态链接Dynaminc Linking(或指向运行时常量池的方法引用)。

在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(Sysmbolic Reference) 保存到class文件的 常量池中。描述一个方法调用用了其他方法时,就是通过常量池中指向方法的符号引用来表示的。

动态链接的作用就是为了将符号引用转换为直接引用的

这不就和类加载系统中链接阶段的解析Resolve过程类似么

相关标签: # JavaSE