什么是虚拟机栈
java虚拟机栈也叫java栈,每个线程在创建时都会创建一个虚拟机栈,其内部保存的是一个个栈帧,对应着一次次的java方法调用
线程私有的
栈中可能出现的异常
JAVA虚拟机规范允许JAVA栈的大小是动态的或是固定不变的
如果采用固定大小的java虚拟机栈,那每一个线程的java虚拟机栈容量可以在线程创建的时候独立选定,如果线程请求分配的栈容量超过java虚拟机栈允许的最大容量,java虚拟机会抛出一个StackOverflowError异常
如果java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新线程时没有足够的内存去创建对应的虚拟机栈,那java虚拟机将会抛出一个OutOfMemoryError异常
JVM直接对java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”的原则
设置栈大小
-Xss参数设置栈大小 可以参考 Oracle官网.
栈帧的内部结构
每个栈帧中存储这
1.局部变量表
2.操作数栈
3.动态链接
4.方法返回地址
5.一些附加信息
下面具体介绍下栈帧中的各个部分
一局部变量表
1.定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,
这些数据类型包括各类数据基本数据类型,对象引用,以及returnAdress类型
2.由于局部变量表是建立在线程的栈上,是线程私有的,因此不存在数据安全问题
3.局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性中,
方法运行期间不会改变局部变量表的大小
下面一段简单的代码用javap命令反编译以后查看局部变量表中信息
public class Demo {
public static void main(String[] args) {
Demo demo = new Demo();
int a = 10;
}
}
反编译后 现在重点在main方法这个栈帧中局部变量表 别的细节以后讨论 在此省略
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1 //locals 就是局部变量表大小 编译器就确定了
0: new #2 // class com/jvm/Demo
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: bipush 10
10: istore_2
11: return
LineNumberTable:
line 10: 0
line 11: 8
line 13: 11
LocalVariableTable: //局部变量表在此
Start Length Slot Name Signature //Slot 局部变量表中索引 Name 方法中的变量 Signature 属性
//Start 代表该变量开始的作用域下面的值代表的是字节码中的行号
//Length 代表该变量作用域的长度
//Start Length 中组合起来解释args 为 形参args 作用域从字节码第0行开始到第12行
//结束 为什么是12 因为main方法字节指令总共12个 下面几个参数也是 8+4=11+1=12
0 12 0 args [Ljava/lang/String; // 形参 args 类型String数组
8 4 1 demo Lcom/jvm/Demo; // 局部变量 demo 类型 com/jvm/Demo
11 1 2 a I // 局部变量 a 类型int
关于slot的理解
局部变量表最基本的存储单元为Slot(变量槽)
局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型,returnAddress类型的变量
在局部变量表种,32位以内的类型只占用一个slot(包括reutrnAddress类型),64位的类型(Long和double)占2个slot
byte short char在存储前被转换成int,boolean也被转换成int
long和slot占两个slot
类的实例方法 在局部变量表中第一个slot是this变量
slot 重复利用
//定义 一个实例方法
public void test2(){
int a = 10;
if (true){
int b = 1;
b = a+1;
}
int c = a +1;
}
//javap -v 查看反编译信息
LocalVariableTable:
Start Length Slot Name Signature
5 4 2 b I
0 14 0 this Lcom/jvm/Demo; //每一个实例方法都会由this变量
3 11 1 a I
13 1 2 c I
可以看到反编译信息中局部变量表长度为3 slot索引为2的槽位被重复利用了 因为变量b和变量c的作用域不冲突
为了节省空间设计的
局部变量表 补充说明
在栈帧中,与性能调优最为密切的部分就是局部变量表,在方法执行时,虚拟机使用局部变量表完成方法的传递
局部变量表中的变量也是重要的垃圾回收根节点,只要局部变量表中直接或间接引用的对象都不会被回收
二 操作数栈
操作数栈和局部变量表一样也是数组实现的只能由出栈入栈操作
操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间
操作数栈就是JVM执行引擎的一个工作区,当一个方法刚开始的时候,一个新的栈帧也会随之被创建出来,这个方法的操作数栈是空的
每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期间就定义好了,保存在方法的Code属性中
栈中的任何一个元素都是可以任意的java数据类型
32bit的类型占一个栈深度单位
64bit的类型占两个栈深度单位
操作数栈并非采用访问索引的方式来进行数据访问的,而只能通过标准的入栈出栈操作
代码分析执行流程
public void test3(){
int a = 10;
int b = 20;
int k = a + b;
}
javap -v 编译后
public void test3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=1
0: bipush 10 //1.将10压入操作数栈
2: istore_1 //2.将操作数栈出栈一个即(10)并加入局部变量表索引为1的位置 为什么不是0?
3: bipush 20 //3.将20压入操作数栈
5: istore_2 //4.将操作数栈出栈一个即(20) 加入局部变量表索引为2的位置
6: iload_1 //5.将局部变量表索引为1的值出栈并入栈操作数栈
7: iload_2 //6.将局部变量表索引为2的值出栈并入栈操作数栈
8: iadd //7。操作数栈出栈两个数据并执行add操作并把结果加入操作数栈
9: istore_3 //8.将操作数栈出栈一个数据并加入局部变量表索引为3的位置
10: return //9方法返回
LineNumberTable:
line 31: 0
line 32: 3
line 33: 6
line 34: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/jvm/Demo;
3 8 1 a I
6 5 2 b I
10 1 3 k I
栈顶缓存技术
三 动态链接
动态链接也叫指向运行时常量池的方法引用
每一个栈帧内部包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法的代码能够实现动态链接 比如 invokedynamic指令
在java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用保存在class文件的常量池中。
动态链接的作用就是将符号引用转换为调用方法的直接引用
class 文件中Constant pool: 会在运行时存放入方法区的运行时常量池
动态链接就是将引用Constant pool:的指针指向方法区的运行时常量池
Constant pool:
#1 = Methodref #8.#32 // java/lang/Object."<init>":()V
#2 = Class #33 // com/jvm/Demo
#3 = Methodref #2.#32 // com/jvm/Demo."<init>":()V
#4 = Double 10.0d
#6 = Double 20.0d
#8 = Class #34 // java/lang/Object
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcom/jvm/Demo;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 demo
#21 = Utf8 a
#22 = Utf8 D
#23 = Utf8 b
#24 = Utf8 test1
#25 = Utf8 I
#26 = Utf8 test2
#27 = Utf8 c
#28 = Utf8 test3
#29 = Utf8 k
#30 = Utf8 SourceFile
#31 = Utf8 Demo.java
#32 = NameAndType #9:#10 // "<init>":()V
#33 = Utf8 com/jvm/Demo
#34 = Utf8 java/lang/Object
方法的绑定
早期绑定 类比 静态链接
指被调用的目标方法如果在编译期可知,且运行期间保持不变
晚期绑定 类比 动态链接
被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法
非虚方法
如果方法在编译期就确定下来具体的调用版本,这个版本在运行时不可改变,这样的方法就叫非虚方法
静态方法,私有方法,final修饰的方法,实例构造器,显式(super)调用父类方法都是非虚方法,也就是不能被重写的方法
其他的都是虚方法
虚拟机中提供了几条方法调用指令
普通调用指令
1.invokestatic 调用静态方法解析阶段确定唯一的方法版本
2.invokespecial 调用init 方法、私有方法、显式(super)父类方法,解析阶段确定唯一父类版本
3.invokevirtual 调用所有虚方法
4.invokeinterface 调用接口方法
动态调用指令
5.invokedynamic 动态解析出需要调用的方法, 然后执行 1.8之后引用 lambda表达式的字节码指令
前四条指令固化在虚拟机内部,方法的调用执行不可认为干预,而invokedynamic指令则支持由用户确定方法版本,其中invokestatic指令和invokespecial指令调用的方法成为非虚方法,其余的(final修饰的除外)成为虚方法
方法重写的本质
1.找到操作数栈顶的第一个元素所执行的对象的实际类型C
2.如果类型C中找到与常量中的描述符和名称都符合的方法,则校验权限,如果通过则直接调用,
如果没有权限,java.lang.IllegalAccessError异常
3.否则 按照继承关系从下往上依次对C的父类进行第2步的搜索和验证过程,
4,如果始终没找到合适的方法,则java.lang.AbstractMethodError异常
虚方法表
在面向对象的编程中,会很频繁的使用到动态分派,如果每次动态分派的过程中都要重新在类的方法元数据中搜索合适的目标的话就可能影响到执行效率。因此,为了提高效率,JVM采用在类的方法区建立一个虚方法表来实现,使用索引表来代替查找。
每个类中都有一个虚方法表,表中存放着各个方法的实际入口
四 方法返回地址
存放调用该方法的pc寄存器的值
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置,
方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令地址。
异常退出时,返回地址是要通过异常表来确定的,栈帧中一般不保存这部分信息
异常表
finally语句在字节码中执行就是在正常返回和异常执行后面copy一份finally字节码指令而保证finally代码必须执行
五 一些附加信息
栈帧中还允许携带与java虚拟机实现相关的一些附加信息。例如,对程序调试提供支持的信息