深入理解JVM虚拟机_4 JVM内部结构分析-栈

作者:田超凡

1. Java 虚拟机栈也是线程私有的,它的⽣命周期和线程相同,描述的是 Java⽅法执⾏的内存模型,每次⽅法调⽤的数据都是通过栈传递的。

2. 虚拟机栈描述的是Java方法执行的线程内存模型每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存储局部变量表、操作数栈、动态连接、方法出口(返回地址)等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

相关代码:


public static void main(String[] args) {

    a();

}

  

  public static void a() {

}

  

  public static void b() {

}

  

  public static void c() {

}



方法压栈与出栈:

栈会遵循先进后出原则 每个方法会创建一个栈帧,在栈帧中存放该方法对应的局部变量表

Idea 调试分析栈帧:

栈帧内部结构原理分析

在每一个方法 栈帧中都会有自己独立的

局部变量表(存放当前方法对应的局部变量);

操作数栈(或表达式栈);

动态连接(或指向运行时常量池的方法引用)

方法出口(或方法正常退出或者异常退出的定义)

栈帧包含方法的所有信息

javap -v Demo03.class

局部变量表

标记清除 标记整理 标记复制  gcroot 引用链

引用计数法

新生代 

com.tcf.days02.Demo04

相关代码:


public static void main(String[] args) {

    String str = "tcf";

    int j = 20;

    double d = 66.66;

    boolean b = true;

}


注意:

1 局部变量表中的变量只在当前方法调用中有效。

2 在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。

3 当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

Variable Slot(变量槽--index)

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

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

注意:short,byte,boolean等数据也占用一个变量槽,因为jvm会在存储时将上述变量转为int类型(变量槽是最基本存储单元,无法分割,只能整个使用)。

1 JVM会为局部变量表中每一个变量分配变量槽,并记录其的存储位置(默认从0开始)。

2 方法的入参也是方法局部变量表中的变量,因此也需要按照参数类型的位数分配变量槽。

Eg:比如main函数方法传递了String [] args 数组,变量args就存储在index为0的变量槽中,变量d因为是64位,需要占用两个变量槽(3和4),变量b因为D占用了两个变量槽,所以直接从index 5处开始存储。

jclasslib分析字节码

1. idea 安装使用

 打开idea 中的settings > plugins 搜索 jclasslib 插件 进行安装 重启生效

重启后点击view  > 选择show bytecode with jclasslib

2.

jclasslib git地址:https://github.com/ingokegel/jclasslib

不赋值(在代码块中赋值)的变量,在局部变量表中不显示,但局部变量表仍会为其预留位置。

相关的代码:


public void tcf001() {

    int a;

    int b;

    int c;

}


变量槽的复用


public static void tcf002() {

    int a = 0;

    {

        int b=30;

        System.out.println("tcf");

    }

    int c = 0;

}



此处原因就是JVM对变量槽有一个复用性为,当变量b超出其作用域后不再生效,所以变量c直接占据了b的位置,所以局部变量表中会少一个位置。

This底层原理

如果当前的方法是实例方法或者是构造方法,则jvm默认会在局部变量表中创建 一个

当前对象 变量名称为 this, 存入在我们当前方法对应的局部表 第0个位置  这样我们就可以在实例方法中 使用 this,静态方法不会。

代码演示:


public void tcf() {

    int j = 20;

}


局部变量表总结

局部变量表只对已确定一定有值的变量和方法参数进行记录,在程序执行中得以直接使用,存储在量变槽中,如果是long和double,则需要占用两个变量槽,实例方法和构造方法会自动创建this变量,并且如果代码块结束(作用域结束),jvm会对变量槽有一个复用的行为,以便于节省空间。

操作数栈分析

相关代码:


public int compute() {

    int a = 10;

    int b = 20;

    int c = (a + b) * 10;

    return c;

}


iconst_0:将int类型的0值压入操作数栈

istore_1: 弹出操作数栈顶的值赋给局部变量表下标为1的变量

iload_1: 将局部变量表下标为1的位置存储的值压入操作数栈

iinc 1 by 1:取局部变量表下标为1的位置存储的值加上1

istore_1:弹出操作数栈顶的值赋给局部变量表下标为1的变量

底层汇编代码:

         0: bipush        10  ##  将一个8位带符号整数压入栈

         2: istore_1       局部变量表中槽1的位置存入10;

         3: bipush        20 ##  将一个8位带符号整数压入栈 20

         5: istore_2       局部变量表中槽2的位置存入20;

         6: iload_1        从局部变量表中槽1的位置 获取 变量a=10;

         7: iload_2        从局部变量表中槽2的位置 获取 变量b=20;

         8: iadd           iadd 执行int类型的加法 10+20

         9: bipush         将一个8位带符号整数压入栈 10

        11: imul           imul 执行int类型的乘法30*10

        12: istore_3        局部变量表中槽3的位置存入300 c=300;

        13: iload_3         最后返回局部变量表中槽3的位置

        14: ireturn

++i与i++的底层原理

i++是先赋值,然后再自增;++i是先自增,后赋值。

i++是直接在局部变量表加的,没有在操作数栈里运算 

I++  与++i底层区别

I++ 先将局部变量表中的值 压入放入到操作数栈中

,在直接对局部变量中做+1操作。

++i 先将局部变量表中的 值 做1+的操作,在将局部变量表中 加1

之后的结果 压入到操作数栈中。

动态连接--常量池

方法出口 定义异常

栈溢出

StackOverflowError(栈溢出)

StackOverflowError代表的是,当栈深度超过虚拟机分配给线程的栈大小时就会出现此error。


public class StackOverFlow {

    private int i;

  

    public void plus() {

        i++;

        plus();

    }

  

    public static void main(String[] args) {

        StackOverFlow stackOverFlow = new StackOverFlow();

        try {

            stackOverFlow.plus();

        } catch (Error e) {

            System.out.println("Error:stack length:" + stackOverFlow.i);

            e.printStackTrace();

        }

    }

}


 在栈空间内存中 是否会发生线程安全问题呢?

动态链接

动态链接: 每个栈帧都保存了 一个可以指向当前方法所在类的 运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接

方法出口

方法返回地址