了解本文需要先了解:对象的内存是如何布局的?
每个方法被执行的时候,java虚拟机都会同步创建一个栈帧,栈的基本单位为栈帧,每个线程都有自已的栈,每个执行方法对应一个栈帧,也叫当前栈帧。每一个栈帧都包括了局部变量表,操作数栈,动态连接,方法返回地址和一些额外的附加信息。
栈的特点就是后进先出,类似于坐电梯,后面进来的先出去。
局部变量表,操作数栈,动态连接,方法返回地址、附加信息相关的了解?
局部变量表
局部变量表是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,局部变量表的容量以变量槽(Variable Slot)为最小单位,基本单位是槽(slot)。
特点:
局部变量的生命周期与栈帧一致:随着方法栈的销毁,局部变量随着销毁。
系统不会为局部变量赋初值:局部变量表的变量是随着java程序被编译成.class,在方法的Code属性max_locals数据项中就确认所需分配的最大容量,局部变量没有准备阶段。也就是说系统不会为局部变量赋初值而实例变量和类变量都会,这个是有区别的!
关于变量槽(Variable Slot)?
《Java虚拟机规范》中没有明确规定变量槽应占用内存的空间大小,但确定变量槽应该存放8种类据类型:
32位或更小内存来存储:boolean、byte、char、short、int、float、reference或returnAddress类型的数据;
64位:long、double 会以高位对齐方式分配两个slot 槽。
注意:变量槽会随着处理器、操作系统或虚拟机的不同而发生变化。比如:在64位虚拟机中使用了64位的物理内存空间去实现一个变量槽,虚拟机仍要使用对齐和补白的手段让变量槽在外观上看起来与32位虚拟机中的一致。
为什么this在static中无法被使用?
public void test1(){
System.out.println("test1");
}
public static void test2(){
System.out.println("test2");
}
通过以上代码得知,this在static中是没有的,而在非static中默认是0,也就是slot中的第一位即0位。
变量槽是是否可以被重复使用?
为了尽可能节省栈帧空间,局部变量中的slot是可以被重复使用的。
通过jclasslib插件获取到 Code中的Code length 。
package com.jvm.slot;
/**
* @author: csh
* @Date: 2021/3/15 16:11
* @Description: 验证 slot被重复使用
*/
public class SlotRepeatUse {
public void test1(){
int one = 0;
int two = 1;
one +=two;
int three = one+2;
}
public void test() {
int one = 0;
{
int two = 1;
one +=two;
}
int three = one+2;
}
}
Maximum local variables 是4.
Maximum local variables 是3.
不管是test1还是test第一位都是this,也就是说this占一位,但是发现test1是4位,而test只有3位,因为代码块中的two被销毁,three才创建,也就是这个槽位被重复使用了。
这里要注意一个问题,虽然说64位占两个槽(slot)位,但是怎么知道是哪两个槽位?虚拟机是通过槽索引来指向的,也就是类似内存地址来指来指向。
操作数栈
操作数栈主要的作用是进行运算,是一个后进先出(Last In First Out,LIFO),通过出入栈进行数据访问(入栈(push)/出栈(pop)算一次操作)。操作数栈32位数据类型栈容量为1,64位数据类型栈容量为2。
区别于局部变量表,操作数栈是通过出入栈来完成一次对数据的访问。这也间接证明了jvm的解释引擎是基于栈式架构而非寄存器架构。
注:
操作数栈最大深度不会超过:max_stacks数据项中设定的最大值
byte、short和char类型在入栈前会被转成int类型;
虽然两个不同帧是相互独立的但是,为了节约一些空间,对栈进行了优化,两个相同的栈之间会有重叠的区域,该区域就可以共享两个栈共用的一部分数据无需额外的参数复制传递。如下图:
什么是栈顶缓存技术?
由于对象创建与访问指令是存储在内存中,频繁的执行读/写会影响执行的速度。所以Hotspot为了解决速度的问题,引入了栈顶缓存(ToS, Top-of-Stack Cashing)技术,将栈顶全部缓存到特理的CPU寄存器来存,并非内存。
有兴趣可以参考源码:https://blog.csdn.net/qq_31865983/article/details/103079839
动态链接
静态链接是什么?
了解动态连接之前先了解一下什么是静态连接,静态连接指的是当一个字节码文件被加载进入JVM后,如果被调用的目标方法就已知,且在运行期间保持不变。这种情况的调用为直接引用称为静态连接。
个人理解:也就是说开始就确定调用方法并且保持不变。比如:出生的时候就确认你的父母是谁了。
什么场景下可以确定是静态链接?
所有的方法调用在Class文件里面存储的都只是符号引用,在类加载解析阶段,会将一部分的符号转化为直接引用,也就是说一开始就确认可调用的版本,并且这个版本在运行期间不可改变,这种称为静态链接或解析(有些书这样讲)。
一开始确定的静态链接有:静态方法、私有方法、实例构造器、父类方法、final修饰方法,也统称叫非虚方法。
那动态链接(Dynamic Linking)也可称为指向运行时常量池的方法引用。如果被调用的方法在编译期无法确定下来,只能够在运行期间确定,那这种称为动态链接。一个方法要调用其他方法,需要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。
动态链接解决了什么问题?
使方法支持动态链接,也就是动态语言的支持。
静态链接或动态链接最终都是通过符号化引用转为直接引用。
关于方法调用
由于不管是静态链接还是动态链接最终的目的都是去调用方法,所以这里结合方法一起了解一下。
调用不同方法支持字节码指令有哪些?
以下就是调用不同的调用方法所需要的字节码指令,也就是说jvm会根据方法所定义的类型通过不同的指令进去调用,通这些指令就是最终决定是静态链接还是动态链接。
invokestatic 用于调用静态方法
invokespecial 用于调用实例构造器()方法、私有方法和父类中的方法
invokevirtual 用于调用所有的虚方法
invokeinterface 用于调用接口方法,会在运行时再确定一个实现该接口的对象4
invokedynamic 先在运行时动态解析出调用点限定符所引用的方法,然后再执行该方法。
分派(Dispatch)
除了静态链接jvm还有一种比较特殊的调用叫做分派,因为这种调用它可能是静态的也可能是动态的。针对方法的分派又分为:静态分派、动态分派、单发派、多分派;
静态分派:由初始化的时候就已经确定,就是与静态链接一致;
动态分派:由运行期间根据实际类型确定方法执行版本的分派过程;
单发派:根据1个宗量对目标方法进行选择;
多分派:根据多个宗量对目标方法进行选择。
宗量:指的是方法接收者与方法参数;
注意:Java语言是一门静态多分派、动态单分派的语言。
更多分派案例可参考:https://www.kancloud.cn/this_is_lxf/javabase/210794
方法返回地址
方法返回地址,主要是存放调用方法的PC寄存器的值,执行方法后返回地址作为程序继续执行的一个依据,让程序得以继续进行下去。当方法执行结束后,有正常和异常两种即出方式,但是不管是正常还是异常都是会回到最初初调用的位置,让程序继续进行。
注意:若调用方法返回的时候带了返回值,其返回会被压入当前栈帧的操作数中,并更新PC寄存器中一条需要执行的字节码指令。
附加信息
JVM还实现增加一些规范里没有描述的信息比如:调试性能收集相关的信息等!
最后
栈的内容非常非常多,主要是通过了解栈相关的信息,了解jvm栈的各个部分功能,待后续了解对象在内存中是如何存储和运算。
参考文章:
https://www.jianshu.com/p/a6a9734ef13d
https://www.it610.com/article/1279940923106541568.htm