JVM学习 运行时数据区 PC寄存器、本地方法栈、虚拟机栈
2、运行时数据区
2.1、程序计数器(PC寄存器)
作用
PC寄存器用来存储指向下一条指令的地址,也就是即将要执行指令的代码。由执行引擎读取下一条指令
注意:每个线程独一份PC寄存器
测试
package com.mhy.day02; public class PCTest01 { public static void main(String[] args) { int i = 10; int j = 20; int k = i + j; System.out.println(k); } }
使用 javap -v PCTest01.class 进行反编译后
Code: stack=2, locals=4, args_size=1 0: bipush 10 2: istore_1 3: bipush 20 5: istore_2 6: iload_1 7: iload_2 8: iadd 9: istore_3 10: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; 13: iload_3 14: invokevirtual #3 // Method java/io/PrintStream.println:(I)V 17: return
2.2、虚拟机栈
基本内容
-
Java虚拟机栈,早期也叫Java栈,每个线程在创建时都会创建一个虚拟机栈,其内存保存一个一个的栈帧,对应一次一次Java方法的调用
注意:每个栈是线程私有的
-
生命周期和线程一致
-
作用:主管Java程序的运行,它保存方法的局部变量、部分结果,并参与方法的调用与返回
栈帧的内部结构
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)(或表达式栈)
- 动态链接(Dynamic Linking)(或指向运行时常量池的方法引用)
- 方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)
- 一些附加信息
1.局部变量表(Local Variables)
- 局部变量表也被称之为局部变量数组或本地变量表
- 定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及returnAddress类型
- 由于局部变量表也是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题
- 局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的Code属性的maxmum local variables数据项中。在方法运行期间是不会改变局部变量表的大小的
- 方法嵌套调用的次数由栈的大小决定
- 局部变量表中的变量只在当前方法调用中有效
测试:
package com.mhy.test1; import java.util.Date; public class LocalVariablesTest { private int count = 0; public static void main(String[] args) { LocalVariablesTest localVariablesTest = new LocalVariablesTest(); int num = 1; localVariablesTest.fun1(); System.out.println(localVariablesTest.count); } public void fun1() { Date date = new Date(); String str = "Hello JVM"; double dd = 0.22; String s = fun2(str, date); System.out.println(s); } public String fun2(String string,Date date){ System.out.println(date); System.out.println(string); System.out.println(fun3()); return "fun2"; } public int fun3(){ int a = 1; { int b = 2; b = a + b; } int c = a + 1; return c; } }
根据编译的class用 jclasslib is a bytecode viewer 来查看结果
先查看main 是static类型的
public static void main(String[] args) { LocalVariablesTest localVariablesTest = new LocalVariablesTest(); int num = 1; localVariablesTest.fun1(); System.out.println(localVariablesTest.count); }
注意:这里局部变量最大槽数是指局部变量占的大小,一般的int、char、引用类型等等占1个槽,long、double等等占2个槽
查看非static类型的方法
public String fun2(String string,Date date){ System.out.println(date); System.out.println(string); System.out.println(fun3()); return "fun2"; }
带有作用域的局部变量
public int fun3(){ int a = 1; { int b = 2; b = a + b; } int c = a + 1; return c; }
变量的分类
- 按照数据类型分类:
- 基本数据类型
- 引用数据类型
- 按照类中声明的位置分类:
- 成员变量:在使用之前都经历过默认初始化阶段
- 类变量:linking 的 prepare 阶段赋默认值 -----> initial 阶段初始化
- 实例变量:随着对象的创建会在堆空间中分配实例变量空间,并进行默认赋值
- 局部变量:在使用前必须赋值,否则编译不通过
- 成员变量:在使用之前都经历过默认初始化阶段
2.操作数栈(Operand Stack)
操作数栈,在方法执行过程中,根据字节码指令,往栈中写入数据或提取数据,即入栈(push)和出栈(pop)
测试
package com.mhy.test1; public class OperandStackTest { public static void main(String[] args) { new OperandStackTest().test(); } public void test(){ byte a = 11; int b = 12; int c = a + b; } }
指令
0 bipush 11 2 istore_1 3 bipush 12 5 istore_2 6 iload_1 7 iload_2 8 iadd 9 istore_3 10 return
代码跟踪
这里可以看出局部变量的长度为4还一个0位置为this,操作数栈的深度为2
3.动态链接(Dynamic Linking)
每一个栈帧内部都包含一个指向运行时常量池中该栈帧所属方法的引用,包含这个引用的目的就是为了支持当前方法代码能够实现动态链接
动态链接的作用就是为了将这些符号引用转换为调用方法的直接引用
4.方法的调用
虚方法与非虚方法
- 非虚方法
- 该方法在编译器就确定了具体的调用版本,这个版本在运行时是不可变的,这样的方法称为非虚方法
- 静态方法、私有方法、final修饰的方法、实例构造器、父类方法都是非虚方法
- 除了上面以外的方法都是虚方法
虚方法与非虚方法的指令
-
普通指令
invokestatic : 调用静态方法,解析阶段确定唯一方法版本 invokepecial : 调用<init>方法、私有及父类方法,解析阶段确定唯一方法版本 invokevirtual : 调用所有虚方法 invokeinterface : 调用所有接口方法 -
动态指令
invokedynamic : 动态解析需要调用的方法,然后执行
方法重写的本质
- 找到操作数栈顶的第一个元素所执行的对象的实际类型,记为 C
- 如果在过程结束后:如果不通类型 C 中找到与常量中的描述符合简单名称都相符的方法,则进行访问权限校验,如果通过就返回这个方法的直接引用,查找过,则返回 java.lang.IllegalAccessError 异常
- 否则,按照继承关系依次从下倒上对 C 的各个父类进行第二步的搜索与校验
- 如果始终没有找到合适的方法,则抛出 java.lang.AbstractMethodError 异常
java.lang.IllegalAccessError 异常
程序试图访问或修改一个属性或者方法,这个属性或者方法你没有权限,就会引起编译时异常,一般在运行时发生
5.方法返回地址(Return Address)
- 存放调用该方法的PC寄存器的值
- 一个方法的结束有两种方式
- 正常执行完成
- 出现未处理的异常,非正常退出
- 正常退出:调用者的PC计数器的值作为返回地址,即调用该方法的指令的下一条指令的地址;异常退出,有异常表来决定返回地址
2.3、本地方法接口
什么是本地方法?
一个 Native Method 就是一个Java调用非java代码的接口。一个 Native Method 就是这样一个方法,该方法非java语言实现,比如说C。
为什么要使用Native Method?
- 与Java环境外交互:有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因
- 与操作系统交互:通过使用本地方法,我们得以用Java实现了jre的与底层系统的交互,甚至JVM的一部分代码都是使用C写的
- Sun Java:Sun的解释器就是用C写的,它使得它能像一些普通的C一样与外部交互
2.4、本地方法栈
Java虚拟机用以管理Java方法的调用,而本地方法栈用于管理本地方法的调用-
- 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限
- 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区
- 它甚至可以直接使用本地处理器中的寄存器
- 直接从本地内存的堆中分配任意数量的内存
- 本不是所有的JVM都支持本地方法,因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方法、数据结构等。
- 在 Hotspot JVM 中,直接将本地方法栈和虚拟机栈合二为一
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!