前面讲过,因为Java虚拟机想要做跨平台的设计,而基于寄存器的结构对不同的CPU是不同的,所以Java的指令都是根据栈来设计的。
下图为广东一道名菜,“佛跳强”的制作方式以及图片,以红色的线为分割,左边我们可以理解为栈区,食材明细可以理解为变量,而做法步骤则理解为操作指令。右边理解为堆区,用来解决存放食材,以及食材怎么放的问题。
Java虚拟机栈(Java Virtual Machine Stack),每个线程在创建的时候,都会创建一个虚拟机栈,内部保存着一个一个的栈帧Stack Frame(栈里存储数据的基本单元),一个栈帧对应着一个Java方法,栈帧里面又包含局部变量表等等。
虚拟机栈的生命周期与线程的创建而创建,与线程的消亡而消亡
主管Java程序的运行,它保存方法的局部变量(8种基本数据类型,对象的引用地址(引用数据对象真正的数据是存在堆空间中的))、部分结果,并参与方法的调用和返回。
解释:在执行方法A的时候,方法A入栈,方法A中又调用方法B,所以方法B入栈变为当前方法,方法B结束后,出栈,然后方法A变为当前方法,方法A结束后,程序结束。
Java 虚拟机规范中指出,允许Java栈的大小是动态的或者是固定不变的。
如果采用固定的大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配栈的容量超过Java虚拟机栈允许的最大容量,这个时候Java虚拟机将会抛出一个StackOverflowError异常。
写一个简单的没有出口的递归方法,会导致栈中出现非常多的栈帧,每个里面都存了很多的局部变量等等,最后导致了StackOverflowError的异常。
public class StackErrorTest { public static void main(String[] args) { main(args); } }
结果,StackOverflowError
Exception in thread "main" java.lang.StackOverflowError at test.StackErrorTest.main(StackErrorTest.java:5) at test.StackErrorTest.main(StackErrorTest.java:5) ...
-Xss[size][unit] 例:-Xss256k
可以使用参数-Xss 来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
public class StackDeepTest{ private static int count=0; public static void recursion(){ count++; recursion(); } public static void main(String args[]){ try{ recursion(); } catch (Throwable e){ System.out.println("deep of calling="+count); e.printstackTrace(); } } }
我们写下面一段程序,有3个方法,互相调用
public class StackFramesTest { public static void main(String[] args) { StackFramesTest.method1(); } public static void method1(){ System.out.println(""); method2(); } public static void method2(){ System.out.println(""); method3(); } public static void method3(){ System.out.println(""); } }
使用IDEA,在method3中打一个断点,进行DEBUG,查看IDEA中显示的Frames的情况,method3为当前栈帧,可以看到方法的调用关系。
当我们结束method3,回到method2 的时候,mehtod3的栈帧就会被丢弃,method2成为当前方法,对应着当前栈帧。
每个栈帧中存储着
并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要是由局部变量表(主要是变量的声明等等)和操作数栈(主要是代码的复杂程度)来决定的。
局部变量表也被称为局部变量数组或本地变量表
举例
对这段代码,先编译为class文件,再对class文件使用javap反编译字节码指令
public class LocalVariables { public static void main(String[] args) { int i = 10; LocalVariables localVariables = new LocalVariables(); System.out.println(i); } }
栈帧中局部变量表的Slot是可以重复利用的,如果一个局部变量表的变量已经过了它的作用域,那么在其作用域之后声明的新的局部变量就很有可能会复用过期局部变量的槽位,从而达到节省资源的目的。
public class SlotTest { public void test4() { int a = 0; { int b = 0; b = a + 1; } int c = a + 1; } //这段代码编译后的局部变量表的长度就会是3,因为c会复用b的槽位 }
按照数据类型分类
按照在类中声明的位置分类
public void test(){ int i; System. out. println(i);//这行代码会编译的时候就报错 }
在栈帧中,与性能调优关系最密切的部分就是局部变量表。
每一个独立的栈帧中除了包含局部变量表之外,还包含一个先进后出的操作数栈,也可以称为表达式栈(Expression Stack)
操作数栈,在方法执行的过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈(push)和出栈(pop)的过程
public class OperandStack { public static void main(String[] args) { byte i = 15; int j =8; int k = i + j; } }
使用javap 命令反编译class文件:javap -v 类名.class
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 15 //往操作数栈中存入byte类型的15,存为int类型 2: istore_1 //从操作数栈中取出int类型的15放入局部变量表为1的位置 3: bipush 8 //8入栈 5: istore_2 //出栈一个,放入局部变量表2的位置 6: iload_1 //加载局部变量表1的位置,并入栈 7: iload_2 //加载局部变量表2的位置,入栈 8: iadd //取出栈中的数字求和,然后再将结果入栈 9: istore_3 //出栈一个,放入局部变量表3的位置 10: return //无返回值,结束 //局部变量表 LocalVariableTable: Start Length Slot Name Signature 0 11 0 args [Ljava/lang/String; 3 8 1 i B 6 5 2 j I 10 1 3 k I
前面提过,基于栈式架构的虚拟机所使用的零地址指令更加紧凑,但完成一项操作的时候必然需要使用更多的入栈和出栈指令,这同时也就意味着将需要更多的指令分派(instruction dispatch)次数和内存读/写次数。
由于操作数是存储在内存中的,因此频繁地执行内存读/写操作必然会影响执行速度。为了解决这个问题,HotSpot JVM的设计者们提出了栈顶缓存(Tos,Top-of-Stack Cashing)技术,将栈顶元素全部缓存在物理CPU的寄存器中,以此降低对内存的读/写次数,提升执行引擎的执行效率。