JVM简图
运行时数据区简图
一、程序计数器(Program Counter Register)
1.程序计数器是什么?
程序计数器是JVM内存模型中的一部分,它可以看作是一个指针,指向当前线程所执行的字节码指令的地址。每个线程在执行过程中都有自己的程序计数器,因此程序计数器是线程私有的,独立于其他线程。
程序计数器不会OOM!!!
2.程序计数器的作用
-
指令执行:在每个线程执行字节码指令时,程序计数器会存储当前正在执行的字节码指令的地址。如果是正在执行本地方法(native method),那么程序计数器的值将是undefined。
-
指令跳转:在字节码指令执行完毕后,程序计数器会自动更新为下一条要执行的字节码指令的地址。通过这种方式,程序计数器可以确保字节码指令按顺序执行。
-
控制流管理:程序计数器帮助管理程序的控制流(如分支、循环、跳转等)。通过更新程序计数器的值,可以实现各种控制流指令(如if、for循环、switch等)的跳转逻辑。
-
多线程切换:由于Java是多线程的语言,每个线程都有自己独立的程序计数器。当线程切换时,程序计数器会保存当前线程的执行位置,当线程再次被调度时,程序计数器会恢复到之前保存的位置,以确保线程可以继续从正确的位置执行。
二、虚拟机栈(Java Virtual Machine Stack)
在Java虚拟机(JVM)中,每个线程在创建时都会创建一个虚拟机栈,虚拟机栈是每个线程私有的数据区,用于管理方法调用和执行。其内部保存一个个的栈帧(Stack Frame),对应着一次次的Java方法调用。每当一个线程调用一个方法时,JVM会为该方法创建一个新的栈帧(Stack Frame)并将其压入虚拟机栈中,方法执行完毕后,栈帧会从栈中弹出。
**存在OOM,但是不需要垃圾回收**
如何设置栈大小
-Xss:一般默认大小为1024KB
单位为bytes,还可以使用KB/MB/GB单位进行设置
栈帧(Stack Frame)
1、JVM直接对Java栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。在一条活动线程中,一个时间点上,只会有一个活动的栈帧。
2、只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(CurrentMethod),定义这个方法的类就是当前类(CurrentClass)。
3、执行引擎运行的所有字节码指令只针对当前栈帧进行操作。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。
4、方法嵌套调用的极限次数,由栈的大小决定。超过大小时就会溢出OOM
1.栈帧的组成部分
- 局部变量表(Local Variable Array/Table)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)
- 方法返回地址(Return Address)
- 附加信息(Additional Information)
2.详细描述
1. 局部变量表(Local Variable Array/Table)
- 原理:
- 局部变量表是一个数组,用于存储方法的局部变量,包括方法参数和方法内部定义的变量。 这些数据类型包括各类基本数据类型、对象引用,以及返回地址。
- 局部变量表是建立在线程的栈上,是线程私有数据,不存在数据安全问题。
- 局部变量表的容量大小,实在编译期间确定下来的,并保存在方法的Code属性的maximum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的。
- 方法嵌套调用的极限次数,由栈的大小决定。参数和局部变量越多,使得局部变量表膨胀,栈帧就越大,就会导致嵌套调用次数减少。
- 表中的变量,只在当前方法调用中有效。方法调用结束后,随着栈帧的销毁,局部变量表随之销毁。
- 作用:为每个方法提供存储和访问局部变量的空间。局部变量通过索引进行访问,索引从0开始。例如,
int a = 10;
中的a
就存储在局部变量表中。 - 存储信息:存储了方法的参数和方法内部定义的局部变量。可以存储各种数据类型,包括基本数据类型(int、float、long、double等)以及对象引用。
大概看一下局部变量表
1、上图中,这里的参数名称、参数类型中的cp_info#,就是符号名称/符号引用,指的就是常量池中的内容。
2、以int 变量 a 为例,19、20 就对应了变量a和int类型
3、需要注意,非静态方法的局部变量表中,第一个序号0一定为this,指向当前方法。静态方法则没有
4、序号是slot,32位的类型占用1个slot,64位的占用两个slot,所以这里的序号都是1递增。如果使用double变量,就会看到序号会+2
5、不足32位的按照32来算,其中byte、short、char、boolean都会被转换为int来储存
2. 操作数栈(Operand Stack)
- 原理:
- 操作数栈是一个LIFO栈,用于字节码指令执行时的临时存储空间。
- 栈的最大深度在编译期就定义好了。并保存在方法的Code属性的max_stack数据中。
- 和局部变量表类似,在栈中,32位的类型占用1个深度,64位的占用两个深度
- 作用:在方法执行过程中,用于保存中间计算结果、传递参数以及存储返回值。例如,执行加法操作
i + j
时,会将i
和j
压入操作数栈,执行完加法操作后,将结果存储在操作数栈中。 - 存储信息:方法执行过程中临时存储的操作数、中间计算结果。
举例
3. 动态链接(Dynamic Linking)
- 原理:每个栈帧包含指向运行时常量池中,该栈帧所属方法的引用,目的是为了支持当前方法的代码能够实现动态链接。
- 作用:
- Java源文件倍编辑成字节码文件时,所有变量和方法引用,都作为符号引用,保存在常量池中
(在上面局部变量表中,有截图)
- 当一个方法调用另外其他方法时,动态链接会将符号引用转换为实际的方法内存地址。
- Java源文件倍编辑成字节码文件时,所有变量和方法引用,都作为符号引用,保存在常量池中
(1)静态链接和动态链接
- 静态链接:
当一个字节码文件被装载进JVM内部时,如果被调用的目标方法在编译期可知
,且运行期保持不变时
。这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。 - 动态链接:
如果被调用的方法在编译期无法被确定下来
,也就是说,只能够在程序运行期
将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。
(2)早期绑定和晚期绑定
对应的方法的绑定机制为:早期绑定(EarlyBinding)和晚期绑定(Late Binding)。绑定是一个字段、方法或者类在符号引用被替换为直接引用的过程,这仅仅发生一次。
- 早期绑定:
早期绑定就是指被调用的目标方法如果在编译期可知,且运行期保持不变时即可将这个方法与所属的类型进行绑定,这样一来,由于明确了被调用的目标方法究竟是哪一个,因此也就可以使用静态链接的方式将符号引用转换为直接引用。 - 晚期绑定
如果被调用的方法在编译期无法被确定下来,只能够在程序运行期根据实际的类型绑定相关的方法,这种绑定方式也就被称之为晚期绑定。
** 这里主要还是针对多态的 **
4. 方法返回地址(Return Address)
- 原理:
- 在方法调用时,返回地址会记录调用方法的指令地址,以便方法返回时能找到正确的返回位置。
- 方法返回有两类:正常完成、异常退出
- 正常返回时,会调用方法的下一条指令
- 异常退出时,需要通过异常表来确定,栈帧不保存相关信息
- 作用:方法执行完毕后,返回到调用该方法的地方继续执行。这个地址一般是调用方法的下一条指令。
- 本质上,方法的退出就是当前栈帧出栈的过程。此时,需要恢复上层方法的局部变量表、操作数栈、将返回值压入调用者栈帧的操作数栈、设置PC寄存器值等,让调用者方法继续执行下去。
- 正常完成出口和异常完成出口的区别在于:通过常完成出口退出的不会给他的上层调用者产生任何的返回值。
5. 附加信息(Additional Information)
- 原理:附加信息因JVM实现而异,包括栈帧的一些其他信息,比如调试信息和性能分析信息。
- 作用:为JVM提供更多的运行时信息支持,如异常处理信息、JVM优化信息等。
课后问答
- 举例栈溢出的情况?(StackOverflowError)
- 调整栈大小,就能保证不出现溢出吗?
- 分配的栈内存越大越好吗?
- 垃圾回收是否会涉及到虚拟机栈?
- 方法中定义的局部变量是否线程安全?