• 145694

    文章

  • 857

    评论

  • 13

    友链

  • 最近新加了换肤功能,大家多来逛逛吧~~~~
  • 喜欢这个网站的朋友可以加一下QQ群,我们一起交流技术。

JAVA 虚拟机栈


概念

  • JVM 虚拟机栈与程序计数器和[本地方法栈]一样都是线程私有的。
  • 栈帧可以理解为一个方法的运行空间

栈帧(Stack Frame)

栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构。每个方法从调用至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

构成

  • 局部变量表
  • 操作数栈
  • 动态连接
  • 方法出口

在代码编译时,栈帧需要多大的局部变量表、多深的操作数栈都是完全确定的(写入到方法表的Code属性中:虚拟机执行子系统 - 类文件构成 - 属性表集合)

当前线程

当前栈帧
Current Stack Frame

局部变量表
Local Variable Table

操作栈
Operand Stack

动态连接
Dynamic Linking

返回地址
Return Address

...... ......

栈帧N

栈帧2

栈帧1

栈帧概念结构图

 

在活动线程中,只有位于栈顶的栈帧才有效,称为当前栈(Current Stack Frame),与这个栈帧相关联的方法称为当前方法(Current Method)。

局部变量表(Local Variable Table)

局部变量表用于存放方法参数和方法内部定义的局部变量。方法表的Code属性: max_locals 数据项指明了该方法所需要分配的局部变量表的最大容量。

局部变量表的容量以Slot(Variable Slot:变量槽)为最小单位,其中64位长度的long和double类型的数据占用2个Slot,其余数据类型(boolean、byte、char、short、int、float、reference、returnAddress)占用一个Slot(一个Slot可以存放32位以内的数据类型)

虚拟机通过索引定位的方式使用局部变量表,索引值的范围是从0到局部变量表最大Slot数量。32位数据类型的变量,索引n就代表使用第n个Slot,如果是64位数据类型的变量,则说明会同时使用n和n+1两个Slot。

在方法执行时,虚拟机是使用局部变量表完成参数值到参数变量列表的传递过程,如果是实例方法(非static的方法)那么局部变量表中第0位的Slot默认是用于传递方法所属实例对象的引用,在方法中可以通过 this 关键字来访问到这个隐含的参数,其余参数则按参数表顺序排列,占用从索引1开始的局部变量Slot。参数列表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

// 关于局部变量表索引与 方法是否被 static 修饰的示例
// 可以通过方法的字节码看到第一次执行store操作的索引值

// 方法 add1 被 static 修饰,则第0位可以直接用于存储参数或者局部变量
public static void add1() {
  int a = 0;
  int b = 3;
  int c = a + b;
}

// 方法 add2 未被 static 修饰,是一个实例变量,则第0位将是方法所属示例对象的引用:this
public void add2() {
  int a = 0;
  int b = 3;
  int c = a + b;
}


// 方法 add1 的字节码,第一次执行istore时,索引为0
0: iconst_0
1: istore_0
2: iconst_3
3: istore_1
4: iload_0
5: iload_1
6: iadd
7: istore_2
8: return



// 方法 add2 的字节码,第一次执行istore时,索引为1,0的位置存放着this
0: iconst_0
1: istore_1
2: iconst_3
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return

Slot 复用

为了节省栈帧空间,局部变量表中的Slot是可以复用的,当方法执行位置已经(程序计数器在字节码的值)超过了某个变量,那么这个变量的Slot可以被其他变量复用。除了能节省栈帧空间,还伴随着可能会影响到系统垃圾收集的行为。


// 示例一
public static void main(String[] args) {
  // 向内存填充64M的数据
  byte[] placeholder = new byte[64 * 1024 * 1024];
  System.gc();
}

// 示例二
public static void main(String[] args) {
  // 向内存填充64M的数据
  {
    byte[] placeholder = new byte[64 * 1024 * 1024];
  }
  System.gc();
}

// 示例三
public static void main(String[] args) {
  // 向内存填充64M的数据
  {
    byte[] placeholder = new byte[64 * 1024 * 1024];
  }
  int a = 0;
  System.gc();
}

编译以上代码添加:verbose:gc 参数查看 GC 收集情况,会发现示例一和示例二的占用并没有被回收,而示例三被成功回收了。

能否被回收的原因在于:局部变量表中的 Slot 是否还存在有关于 placeholder 数组的关联。在示例三中,变量a复用了已经不再使用的 placeholder 变量的Slot,在 GC 执行时,就可以正常回收 placeholder 除去a复用的空间。

所以,在部分情况下给不在使用变量设置为null,也不能算完全没有意义。不过应当以恰当的作用域来避免使用手动设置null的方式(实际上在经过JIT的编译优化后,手动设置为null语句将被消除掉)。

局部变量表大小对调用的影响

局部变量表是存在栈帧中的,如果方法的参数列表和局部变量比较多,那么调用一次会占用更多的栈空间,将会导致在栈空间内存一定下调用次数减少。

public class TestLocalVariableTimes {

    private static int count = 0;

    public static void recursion(int a, int b, int c) {
        long l1 = 12;
        short sl = 1;
        byte b1 = 1;
        String s = "1";
        System.out.println("count=" + count);
        count++;
        recursion(1, 2, 3);
    }

    public static void recursion() {
        System.out.println("count=" + count);
        count++;
        recursion();
    }

    public static void main(String[] args) {
        recursion(1, 2, 3);
    }
}

以上代码中,第一个方法有3个参数4个变量,由于long是占用2个slot,所以占用8个Slot,而第二个方法没有参数和局部变量,则不占用Slot。可以通过 javap -verbose TestLocalVariableTimes 查看每个方法的locals值。

通过 java -Xss160K TestLocalVariableTimes 运行调用不同方法时可以发现,无参数无局部变量的方法明显比有参数的方法执行的次数多,所以局部变量表的大小对调用次数是有直接关系的。

操作数栈(Operand Stack)

操作数栈也常被称为操作栈,它是一个后入先出(Last In First Out:LIFO)栈。同局部变量表一样,操作数栈的最大深度也在编译的时候就写入到了Code属性的 max_stacks 数据项中。

操作数栈的每一个元素可以是任意的 Java 数据类型,包括 long 和 double。32位数据类型所占的栈容量为1,64位数据类型所占用栈容量为2。

CPU 执行时是通过从寄存器取指令,而 JVM 的指令主要是从操作数栈取的,因此 JVM 是一个基于栈而不是基于寄存器的执行引擎。当执行到一个方法时,这个方法的操作数栈是空的,方法的执行也就对应着相关指令的入栈、出栈。

基本执行逻辑

主要是对变量值的入栈、出栈、运算、入栈…

例如将两个int类型的局部变量相加再将结果保存至第三个局部变量:

// 源代码
public static void main (String[] args) {
  int a = 0;
  int b = 3;
  int c = a + b;
}

// 字节码
0: iconst_0   // 将常量加载到操作数栈中
1: istore_1   // 将上一步加载到操作数栈的常量数值存储到局部变量表中
2: iconst_3   // 同0
3: istore_2   // 同1
4: iload_1    // 将局部变量表索引为1的值加载到操作数栈中
5: iload_2    // 同4
6: iadd       // 对最接近栈顶的两个值出栈并进行求和然后将结果入栈
7: istore_3   // 将相加的结果从操作数栈存储到局部变量表
8: return     // 方法正常返回,结束

操作数栈中元素的类型必须与字节码指令的序列严格匹配,例如上面的iadd操作时,不能出现iadd操作需要的值第一个为long 第二个为 float 的情况。

参考资料


695856371Web网页设计师②群 | 喜欢本站的朋友可以收藏本站,或者加入我们大家一起来交流技术!

0条评论

Loading...


发表评论

电子邮件地址不会被公开。 必填项已用*标注

自定义皮肤 主体内容背景
打开支付宝扫码付款购买视频教程
遇到问题联系客服QQ:419400980
注册梁钟霖个人博客