虚拟机栈特点
虚拟机栈出现的背景:
- 由于跨平台性的设计,Java的指令都是根据栈来设计的。不同平台CPU架构不同,所以不能设计成基于寄存器的。
内存中的栈与堆:
栈是运行时的单位,堆是存储的单位。即栈解决程序运行的问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题。
虚拟机栈的基本内容
-
Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个栈帧(Stack Frame),每个栈帧对应着一次的Java方法调用。Java虚拟机栈是线程私有的。
-
Java虚拟机栈的生命周期同线程一致
一句话概述虚拟机栈:
Java虚拟机栈主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。Java虚拟机栈保存的局部变量只是八种基础数据类型,Java虚拟机栈中还保存引用数据类型(类、数组、接口)的地址。
栈的特点:
- 栈是一种快速有效的分配存储方式,访问速度仅次于程序计数器。
- JVM 直接对Java栈的操作只有两个,每个方法执行,伴随着进栈(入栈、压栈)与 执行结束后的出栈工作
- 对于栈来说不存在垃圾回收问题,但是可能会出现内存溢出(OOM)
- 线程请求的栈深度大于虚拟机所允许的最大深度时会抛出
StackOverflowError
异常。 - 虚拟机在扩展栈时无法申请足够的内存空间会抛出
OutOfMemoryError
异常。 - 虚拟机栈的优点是跨平台,指令集小,编译器容易实现;缺点是性能下降,实现同样的功能需要更多的指令。
- 栈是先进后出,队列是先进先出
虚拟机栈配置
JVM提供了-Xss
来指定线程的最大栈空间, 该参数也直接决定了函数调用的最大深度.
测试代码:
package com.jvm.study;
public class StackTest2 {
private static int i = 0;
private static void foo() {
i++;
foo();
}
public static void main(String[] args) {
try {
foo();
} catch (Throwable t) {
System.out.println(i);
}
}
}
配置VM options:
-Xss128M对应执行结果:
8344641
-Xss1M对应执行结果:
23096
默认配置对应执行结果:
23359
栈帧存储数据类型
栈帧内部“数据结构”主要由这几个部分组成:局部变量表、操作数栈、动态链接、方法出口等信息。
- 局部变量表
存放基本数据类型变量、引用类型的变量、returnAddress类型的变量
- 操作数栈
在执行字节码指令过程中被用到,这种方式类似于原生 CPU 寄存器。
大部分 JVM 字节码把时间花费在操作数栈的操作上:入栈、出栈、复制、交换、产生消费变量的操作。
因此,局部变量数组和操作数栈之间的交换变量指令操作通过字节码频繁执行。
比如,一个简单的变量初始化语句将产生两条跟操作数栈交互的字节码。
- 动态链接
每个栈帧都有一个运行时常量池的引用。这个引用指向栈帧当前运行方法所在类的常量池(保存在方法区)。通过这个引用支持动态链接(dynamic linking)。
当 Java 类文件编译时,所有变量和方法的引用都被当做符号引用存储在这个类的常量池中。
符号引用是一个逻辑引用,实际上并不指向物理内存地址。
JVM 可以选择符号引用解析的时机,一种是当类文件加载并校验通过后,这种解析方式被称为饥饿方式。另外一种是符号引用在第一次使用的时候被解析,这种解析方式称为惰性方式。无论如何 ,JVM 必须要在第一次使用符号引用时完成解析并抛出可能发生的解析错误。
绑定是将对象域、方法、类的符号引用替换为直接引用的过程。绑定只会发生一次。一旦绑定,符号引用会被完全替换。如果一个类的符号引用还没有被解析,那么就会载入这个类。
每个直接引用都被存储为相对于存储结构(与运行时变量或方法的位置相关联的)偏移量。
- 方法出口信息
- 附加信息
测试执行中栈中操作代码
public class StackTest2 {
public static void foo() {
int a = 1;
int b = 2;
int c = (a + b) * 5;
}
public static void main(String[] args) {
foo();
}
}
反编译文件
public class StackTest2 {
public StackTest2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void foo();
Code:
0: iconst_1
1: istore_0
2: iconst_2
3: istore_1
4: iload_0
5: iload_1
6: iadd
7: iconst_5
8: imul
9: istore_2
10: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #2 // Method foo:()V
3: return
}
16进制查看class文件为
main方法执行后,基于栈的Hotspot的执行过程如下:
注:求值栈就是操作数栈
基于寄存器的DalvikVM执行过程如下所示:
上述两种方式最终通过JVM执行引擎,CPU接收到的汇编指令是:
栈运行原理
- JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”的原则。
- 在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。程序计数器也是存储的也是当前栈帧的指令地址。
- 执行引擎运行的所有字节码指令只针对当前栈帧进行操作。
- 如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,称为新的当前栈帧。
- 不同线程中所包含的栈帧是不允许存在互相引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。栈是线程私有的,不允许互相共享
- 如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。
- Java方法有两种返回函数的方式,一种是正常函数返回,使用return指令;另一种是抛出异常(有异常但是没处理)。不管使用哪一种方式,都会导致栈帧被弹出。
示例程序
package com.jvm.study;
public class StackFrameTest {
public static void main(String[] args) {
StackFrameTest frameTest = new StackFrameTest();
frameTest.method1();
}
public void method1() {
System.out.println("Method1开始执行。。。");
method2();
System.out.println("Method1结束执行。。。");
}
public void method2() {
System.out.println("Method2开始执行。。。");
method3();
System.out.println("Method2结束执行。。。");
}
public void method3() {
System.out.println("Method3开始执行。。。");
System.out.println("Method3结束执行。。。");
}
}
执行结果:
Method1开始执行。。。
Method2开始执行。。。
Method3开始执行。。。
Method3结束执行。。。
Method2结束执行。。。
Method1结束执行。。。
以上示例程序对应的栈的示意图:
网友评论