文章目录
一、虚拟机栈概述
1.1 虚拟机栈的背景
1.2 内存中的堆与栈
1.3 虚拟机栈的基本内容
1.4 栈中可能出现的异常
1.5 设置栈的内存大小
二、栈的存储单位
2.1 Java虚拟机栈的存储结构和运行原理
2.2 栈帧的内部结构
三、局部变量表(Local Variables)
3.1 查看帧的局部变量表
3.2 变量槽slot的理解与演示
3.3 slot的重复利用
3.4 静态变量与局部变量的对比及小结
3.5 补充说明
四、操作数栈(Operand Stack)
4.1 操作数栈特点
五、代码追踪
六、栈顶缓存技术(Top Of Stack Cashing)
七、动态链接(Dynamic Linking)
八、方法的调用:解析和分派
8.1 静态链接与动态链接
8.2 早期绑定与晚期绑定
8.3 多态性与方法绑定
8.4 方法重写的本质
8.5 多态与虚方法表
九、方法返回地址
十、相关面试题
一、虚拟机栈概述
1.1 虚拟机栈的背景
由于跨平台性的设计,Java的指令都是根据栈来设计的 。不同平台CPU架构不同,所以不能设计为基于寄存器的。
根据栈设计的优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。
1.2 内存中的堆与栈
栈是运行时的单位,而堆是存储的单位。
1.栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。堆解决的是数据存储的问题,即数据怎么放、放在哪儿。
2.一般来讲,对象主要都是放在堆空间的,是运行时数据区比较大的一块
3.栈空间存放基本数据类型的局部变量,以及引用数据类型的对象的引用
1.3 虚拟机栈的基本内容
1.Java虚拟机栈(Java Virtual Machine Stack),早期也叫Java栈。 每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应这个一次次的Java方法调用。它是线程私有的。
2.生命周期和线程是一致的
3.栈是一种快速有效的分配存储方式,访问速度仅次于PC寄存器(程序计数器)
4.作用:主管Java程序的运行,它保存方法的局部变量、8种基本数据类型、对象的引用地址、部分结果,并参与方法的调用和返回。
局部变量:相较于成员变量(成员变量或称属性)
基本数据变量:8种基本数据类型
引用类型变量:类、数组、接口
5.JVM直接对J ava栈的操作只有两个
(1)每个方法执行,伴随着 进栈(入栈,压栈)
(2)执行结束后的 出栈 工作
6.对于栈来说不存在垃圾回收问题,但是存在OOM异常
下面接着说Java虚拟机栈的异常。
1.4 栈中可能出现的异常
1.Java虚拟机规范允许Java栈的大小是 动态的或者是固定不变的
2.如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。
3.如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个 StackOverFlowError异常。
/** * 演示栈中的异常 */ public class StackErrorTest { public static void main(String[] args) { main(args); } }
4.如果Java虚拟机栈可以动态拓展,并且在尝试拓展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个 OutOfMemoryError异常。
1.5 设置栈的内存大小
我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。 (IDEA设置方法:Run-EditConfigurations-VM options 填入指定栈的大小-Xss256k)。
-Xss1024m // 栈内存为 1024MBS -Xss1024k // 栈内存为 1024KB
设置线程的最大栈空间:256KB
/** * 演示栈中的异常 * * 默认情况下:count 10818 * 设置栈的大小: -Xss256k count 1872 */ public class StackErrorTest { private static int count = 1; public static void main(String[] args) { System.out.println(count); count++; main(args); } }
递归 2471 次,栈内存溢出
二、栈的存储单位
2.1 Java虚拟机栈的存储结构和运行原理
栈存储什么?
1.每个线程都有自己的栈,栈中的数据都是以 栈帧 (Stack Frame)的格式存在
2.在这个线程上 正在执行的每个方法都对应各自的一个栈帧(Stack Frame)
3.栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息
栈的运行原理
4.JVM直接对Java栈的操作只有两个,就是对栈帧的 压栈和出栈,遵循先进后出/后进先出的和原则。
5.在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧对应的方法就是当前方法(Current Frame)
6.执行引擎运行的所有字节码指令只针对当前栈帧进行操作
7.如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前栈帧。
8.不同线程中所包含的栈帧是不允许相互引用的,即不可能在另一个栈帧中引用另外一个线程的栈帧
9.如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧
10.Java方法有两种返回函数的方式,一种是正常的函数返回,使用return指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。
代码示例:
代码
/** * 栈帧 */ public class StackFrameTest { public static void main(String[] args) { StackFrameTest test = new StackFrameTest(); test.method1(); //输出 method1()和method2()都作为当前栈帧出现了两次,method3()一次 // method1()开始执行。。。 // method2()开始执行。。。 // method3()开始执行。。。 // method3()执行结束。。。 // method2()执行结束。。。 // method1()执行结束。。。 } public void method1(){ System.out.println("method1()开始执行。。。"); method2(); System.out.println("method1()执行结束。。。"); } public int method2(){ System.out.println("method2()开始执行。。。"); int i = 10; int m = (int) method3(); System.out.println("method2()执行结束。。。"); return i+m; } public double method3(){ System.out.println("method3()开始执行。。。"); double j = 20.0; System.out.println("method3()执行结束。。。"); return j; } }
先执行的函数,最后执行结束
method1()开始执行... method2()开始执行... method3()开始执行... method3()即将结束... method2()即将结束... method1()执行结束...
反编译,可以看到每个方法后面都带有 return 语句或者 ireturn 语句
public void method1(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 3: ldc #6 // String method1()开始执行... 5: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: aload_0 9: invokevirtual #8 // Method method2:()I 12: pop 13: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream; 16: ldc #9 // String method1()执行结束... 18: invokevirtual #7 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 21: return LineNumberTable: line 16: 0 line 17: 8 line 18: 13 line 19: 21 LocalVariableTable: Start Length Slot Name Signature 0 22 0 this Lcom/atguigu/java1/StackFrameTest;
2.2 栈帧的内部结构
每个栈帧中存储着
1.局部变量表(Local Variables)
2.操作数栈(Operand Stack)(或表达式栈)
3.动态链接(Dynamic Linking)(或执行"运行时常量池"的方法引用)----深入理解Java多态特性必读!!
4.方法返回地址(Return Adress)(或方法正常退出或者异常退出的定义)
5.一些附加信息
其中部分参考书目上,称方法返回地址、动态链接、附加信息为帧数据区。
并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的。
三、局部变量表(Local Variables)
1.局部变量表也被称之为 局部变量数组或本地变量表
2.定义为一个数字数组,主要用于 存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddressleixing
3.由于局部变量表是建立在线程的栈上,是线程私有的数据,因此 不存在数据安全问题
4.局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
5.方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。
1.对一个函数而言,他的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。
2.进而函数调用就会占用更多的栈空间。
6.局部变量表中的变量只在当前方法调用中有效。
1.在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。
2.当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
3.1 查看帧的局部变量表
利用javap命令对字节码文件进行解析查看main()方法对应栈帧的局部变量表,如图:
也可以在IDEA 上安装jclasslib byte viewcoder插件查看方法内部字节码信息剖析,以main()方法为例
3.2 变量槽slot的理解与演示
1.参数值的存放总是 从局部变量数组的index0开始,到数组长度-1的索引结束。
2.局部变量表,最基本的存储单元是Slot(变量槽)。
3.局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
4.在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
byte、short、char、float在存储前被转换为int,boolean也被转换为int,0表示false,非0表示true;
long和double则占据两个slot。
5.JVM会为局部变量表中的每一个slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值
6.当一个实例方法被调用的时候,它的方法参数和方法体内部定义的局部变量将会按照声明顺序被复制到局部变量表中的每一个slot上
7.如果需要访问局部变量表中一个64bit的局部变量值时,只需要使用前一个索引即可。(比如:访问long或者double类型变量)
8.如果当前帧是由 构造方法或者实例方法 创建的(意思是当前帧所对应的方法是构造器方法或者是普通的实例方法),那么该对象引用this将会存放在index为0的slot处,其余的参数按照参数表顺序排列。
9.静态方法中不能引用this,是因为静态方法所对应的栈帧当中的局部变量表中不存在this
示例代码:
public class LocalVariablesTest { private int count = 1; //静态方法不能使用this public static void testStatic(){ //编译错误,因为this变量不存在与当前方法的局部变量表中!!! System.out.println(this.count); } }
3.3 slot的重复利用
栈帧中的局部变量表中的槽位是可以重复利用的,如果一个局部变量过了其作用域,那么在其作用域之后申明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
private void test2() { int a = 0; { int b = 0; b = a+1; } //变量c使用之前以及经销毁的变量b占据的slot位置 int c = a+1; }
上述代码对应的栈帧中局部变量表中一共有多少个slot,或者说局部变量表的长度是几?
答案是3
变量b的作用域是
{ int b = 0; b = a+1; }
this占0号、a单独占1个槽号、c重复使用了b的槽号
3.4 静态变量与局部变量的对比及小结
变量的分类:
按照数据类型分:
①基本数据类型
②引用数据类型
按照在类中声明的位置分:
①成员变量:在使用前,都经历过默认初始化赋值
static修饰:类变量:类加载链接的准备preparation阶段给类变量默认赋0值——>初始化阶段initialization给类变量显式赋值即静态代码块赋值;
不被static修饰:实例变量:随着对象的创建,会在堆空间分配实例变量空间,并进行默认赋值
②局部变量:在使用前,必须要进行显式赋值的!否则,编译不通过。
3.5 补充说明
在栈帧中,与性能调优关系最为密切的部分就是局部变量表。在方法执行时,虚拟机使用局部变量表完成方法的传递。
局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收。