受多种情况的影响,又开始看JVM 方面的知识。
1、Java 实在过于内卷,没法不往深了学。
2、面试题问的多,被迫学习。
3、纯粹的好奇。 很喜欢一句话:“八小时内谋生活,八小时外谋发展。”
--- 望别日与君相见时,君已有所成。
共勉
封面地点
:湖南邵阳
作者
:博主🤸♂️🤸♂️🤸♂️
一、虚拟机栈概述
先给大家来看一下 运行时数据区的图示👇
如果大家没咋了解Java的内存结构,就常会粗粒度地将JVM中的内存区理解为仅有Java堆(heap)和Java战(stack)?为什么?🤳🧐
首先栈是运行时的单位,而堆是存储的单位
- 栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。
- 堆解决的是数据存储的问题,即数据怎么放,放哪里
不过今天我们讨论的是虚拟机栈。堆的文章之后才更👨💻。
虚拟机栈
:java虚拟机栈是线程私有的,他与线程的声明周期同步。虚拟机栈描述的是java方法执行的内存模型,每个方法执行都会创建一个栈帧,栈帧包含局部变量表、操作数栈、动态连接、方法出口等。
注意:🏂
- 它的执行速度仅次于程序计数器
- 对于栈来说不存在垃圾回收问题
- 主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用和返回。
二、栈帧
2.1、栈与栈桢:
每一个方法的执行到执行完成,对应着一个栈帧在虚拟机中从入栈到出栈的过程。 👨🚀 1、java虚拟机栈栈顶的栈帧就是当前执行方法的栈帧,PC寄存器会指向该地址。👇 2、当这个方法调用其他方法的时候就会创建一个新的栈帧,这个新的栈帧会被方法Java虚拟机栈的栈顶,变为当前的活动栈,在当前只有当前活动栈的本地变量才能被使用, 3、当这个栈帧所有指令都完成的时候,这个栈帧被移除,之前的栈帧变为活动栈,前面移除栈帧的返回值变为这个栈帧的一个操作数。
2.2、栈帧概述
栈帧(Stack Frame)
是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素。每个栈帧都包含了:
- 局部变量表
- 操作数栈(或表达式栈)
- 动态连接 (或指向运行时常量池的方法引用)
- 方法返回地址(或方法正常退出或者异常退出的定义)
- 一些额外的附加信息
👩💻 在编译代码的时,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了,并且写入到了方法表的Code属性中,因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体虚拟机的实现。
如下图:
左边是通过javap -v 类名的.class
命令反编译出来的。==注意啊,得在生成目标文件夹目录下👇==
我们通过上图可以看到,在编译过程中,已经给每个栈桢分配好了 操作数栈 的深度啊,局部变量表的大小等等。
局部变量表是4的原因:虽然我们在这个方法中只定义了a,b,c 三个局部变量,但是大家还记得 this
吗,你没有想错,确实在这个局部变量表中,第一个是this。更深层次解释不了,技术不够。😂。到局部变量部分带大家一起看。
在这里插一句哈,如果大家不熟悉这种命令行去反编译的话,在这里介绍一个idea 的插件。
放心哈,那插件作者肯定没给我打钱,我是感觉真的挺可的,对于我们新手学习这方面。
名字:
jclasslib Bytecode Viewer
用法
我们将一个类编译完后,兄弟们,编译没有问题吧。不行,感觉还是要贴出来哈。
编译完成之后。我们打开 菜单中 -->view 选项。
里面的具体的东东靠大家慢慢发掘了哈,我们还是回归正文啦。==给大家个Oracle 的JVM 官方规范。方便指令的查找解释哈。==
2.3、想一想我们遇到过哪些与栈相关的异常?
Java 虚拟机规范允许Java栈的大小是动态的或者是固定不变的。 🏄♂️
1、如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,Java虚拟机将会抛出一个``StackoverflowError `异常。(栈溢出)
举个栗子:相信大家肯定学过递归算法,如果它一直没有出口,结果就是栈溢出。🚣♂️
2、如果Java虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那Java虚拟机将会抛出一个 OutOfMemoryError
异常。(也就是内存溢出异常OOM)
2.4、设置栈内存大小
刚刚大家也看到了,我们可以使用参数 -Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。
-Xss1m // m 是Mb -Xss1k // k 是Kb
2.5、局部变量表
概述:
局部变量表:Local Variables
,被称之为局部变量数组或本地变量表。定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型。局部变量表是线程私有的。
局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。
👨💻👨🚀🤹♂️🤽♂️🏌️♂️🐱🚀🐱🐉🎊🧬🚀🛫🚢🛸🚤⛲🌈🌊 悄咪咪试一波小表情,课间休息会,怕看疲惫了,就放弃继续看下去啦哈。🤗
局部变量的存放 Slot(变量槽)
参数值的存放总是在局部变量数组的index0开始,到数组长度-1的索引结束。🤭
局部变量表,最基本的存储单元是Slot(变量槽)局部变量表中存放编译期可知的各种基本数据类型(8种),引用类型(reference),returnAddress类型的变量。
在局部变量表里,32位以内的类型只占用一个slot(包括returnAddress类型),64位的类型(long和double)占用两个slot。
JVM会为局部变量表中的每一个Slot都分配一个访问索引,通过这个索引即可成功访问到局部变量表中指定的局部变量值。
静态变量与局部变量的对比
变量的分类:😜
- 按数据类型分:基本数据类型、引用数据类型
- 按类中声明的位置分:成员变量(类变量,实例变量)、局部变量
- 类变量:linking的paper阶段,给类变量默认赋值,init阶段给类变量显示赋值即静态代码块
- 实例变量:随着对象创建,会在堆空间中分配实例变量空间,并进行默认赋值
- 局部变量:在使用前必须进行显式赋值,不然编译不通过。
🎅
我们知道类变量表有两次初始化的机会,第一次是在“准备阶段”,执行系统初始化,对类变量设置零值,另一次则是在“初始化”阶段,赋予程序员在代码中定义的初始值。
和类变量初始化不同的是,局部变量表不存在系统初始化的过程。这意味着如果创建了局部变量,并且在使用前不对它进行显示赋值,那么将无法通过编译。
在栈帧中,与性能调优关系最为密切的部分就是前面提到的局部变量表
。在方法执行时,虚拟机使用局部变量表完成方法的传递。 🛀局部变量表中的变量也是重要的垃圾回收根节点,只要被局部变量表中直接或间接引用的对象都不会被回收
2.6、操作数栈
1、每一个独立的栈帧除了包含局部变量表以外,还包含一个后进先出(Last - In - First -Out)的 操作数栈,也可以称之为 表达式栈(Expression Stack)🤪
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和 出栈(pop)
- 某些字节码指令将值压入操作数栈,其余的字节码指令将操作数取出栈。使用它们后再把结果压入栈
- 比如:执行复制、交换、求和等操作
🤦♂️ 2、操作数栈,主要用于保存计算过程的中间结果,同时作为计算过程中变量临时的存储空间。
3、每一个操作数栈都会拥有一个明确的栈深度用于存储数值,其所需的最大深度在编译期就定义好了,保存在方法的Code属性中,为maxstack的值。
4、操作数栈的每一个元素可以是任意Java数据类型,32位的数据类型占一个栈容量,64位的数据类型占2个栈容量,且在方法执行的任意时刻,操作数栈的深度都不会超过max_stacks中设置的最大值。
5、操作数栈并非采用访问索引的方式来进行数据访问的,而是只能通过标准的入栈和出栈操作来完成一次数据访问
6、如果被调用的方法带有返回值的话,其返回值将会被压入当前栈帧的操作数栈中,并更新PC寄存器中下一条需要执行的字节码指令。
public void stackTest() { int a = 10; int b = 21; int c = a + b; }
0 bipush 10 // 10被压入操作数堆栈。 2 istore_1 //从操作数堆栈中弹出一个数 ,将这个数赋值给局部变量 a 这里istore_的索引之所以是一,而不是0,是因为局部变量表中,第一个放进去的是this。 static方法中 没有 this,那个时候索引才是从0开始。 3 bipush 21 5 istore_2 // 同上 6 iload_1 // iload_<n> <n > 必须是当前帧的局部变量数组的索引。< n >处的局部变量必须包含一个int. < n >处的局部变量的值被压入操作数堆栈。 7 iload_2 8 iadd // 执行 add 操作 9 istore_3 // 将结果赋值到局部变量 索引为3的变量上。 10 return
我想看完这个gif动图 ,我想大家大概能够明白操作数栈是一个什么样的流程了吧,或者已经明白了吧。如果没有明白的话,可以留言评论哈。
2.7、动态链接
概述
动态链接(Dynamic Linking)
: 每个栈帧都保存了 一个 可以指向当前方法所在类的 运行时常量池, 目的是: 当前方法中如果需要调用其他方法的时候, 能够从运行时常量池中找到对应的符号引用, 然后将符号引用转换为直接引用,然后就能直接调用对应方法, 这就是动态链接。 比如:invokedynamic指令 👍
在Java源文件被编译到字节码文件中时,所有的变量和方法引用都作为符号引用(symbolic Reference)保存在class文件的常量池里。
小思考:为什么需要运行时常量池?
因为在不同的方法,都可能调用常量或者方法,所以只需要存储一份即可,节省了空间。
常量池的作用:就是为了提供一些符号和常量,便于指令的识别
比如:描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的,那么动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用。
讲这么这么多,没有亲眼见过,其实还是会对所谓的动态链接感到陌生的,因为我也是的,所以接下来👇 给大家举了栗子和图哦。
1、代码部分
2、通过 javap -v 类名.class
进行反编译后
- main 方法
描述一个方法调用了另外的其他方法时,就是通过常量池中指向方法的符号引用来表示的
。
- 注意图中 调用test 方法中的那一行指令 invokevirtual #6 // Method test:()V
- invokevirtual #6 :调用实例方法;基于类调度 。
- 那么#6是什么意思呢? 这就牵扯到了常量池啦。
- 我们接着来看一下 常量池(Constant pool)
- #6 又接着指向了 #4.#33 但其实 # 6 后面的注释已经讲出来了。//
StackFrameTest.test:()V
- #4 是 Class, StackFrameTest 实例。
- #33 又接着执行#15:#9 也就是后面的注解 // test:() V
- test 说的是方法名 ()V 说的返回值是 void。
链接
静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期克制,且运行期保持不变时,这种情况下降调用方法的符号引用转换为直接引用的过程称之为静态链接
动态链接:
如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用的方法的符号转换为直接引用,由于这种引用转换过程具备动态性,因此也被称之为动态链接。
这个动态链接只从粗略的角度讲了,里面其实还有一些内容没讲,考虑到篇幅过长,有时间会再补一篇动态链接的文章。
2.8、方法返回地址
存放调用该方法的pc寄存器的值。当一个方法开始执行后,只有两种方式可以退出这个方法:
- 正常完成出口:执行引擎遇到任意一个方法返回的字节码指令(return),会有返回值传递给上层的方法调用者,简称正常完成出口;究竟需要使用哪一个返回指令,还需要根据方法返回值的实际数据类型而定。
- 异常完成出口 :在方法执行过程中遇到异常(Exception),并且这个异常没有在方法内进行处理,也就是只要在本方法的异常表中没有搜索到匹配的异常处理器,就会导致方法退出,简称异常完成出口。
无论通过哪种方式退出,在方法退出后都返回到该方法被调用的位置。方法正常退出时,调用者的pc计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址。而通过异常退出的,返回地址是要通过异常表来确定,栈帧中一般不会保存这部分信息。
异常表:
方法执行过程中,抛出异常时的异常处理,存储在一个异常处理表,方便在发生异常的时候找到处理异常的代码
本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
正常完成出口和异常完成出口的区别在于:通过异常完成出口退出的不会给他的上层调用者产生任何的返回值
2.9、一些附加信息
栈帧中还允许携带与Java虚拟机实现相关的一些附加信息。例如:对程序调试提供支持的信息。
课间休息会哈 接着看题啦
面试
面试提问:
1、这个栈内存大小是设置的越大越好吗????是的话,是为什么?不是的话,又是为什么?
- 不是,一定时间内降低了OOM概率,但是会挤占其它的线程空间,因为整个空间是有限的。
2、垃圾回收是否涉及到虚拟机栈?
- 不会
3、方法中定义的局部变量是否线程安全?
- 具体问题具体分析。看到这一点你可能会产生一些疑惑,我也理解。
- 为什么会产生疑惑呢?我讲过局部变量表是线程私有的,竟然都是私有的,肯定是线程安全的啊,但是这有一个前提的,如果这个局部变量在方法内部产生,又在方法内部消亡,生命周期是和栈桢相同的,那么可以肯定是它是线程安全的。但是如果这个方法是需要接收参数,或者是需要返回值,那么这个时候就可以需要具体分析啦。
自言自语
兄弟们,还是一起躺平吧。内卷太累辣吧。。。 👩💻->👨💻🛌🛌