《深入理解Java虚拟机》第八章学习笔记:虚拟机字节码执行引擎

发布时间:2026/11/3 19:32:21

《深入理解Java虚拟机》第八章学习笔记:虚拟机字节码执行引擎 8.1 概述虚拟机的执行引擎则是由软件自行实现的因此可以不受物理条件制约地定制指令集与执行引擎的结构体系能够执行那些不被硬件直接支持的指令集格式从外观上来看所有的Java虚拟机的执行引擎输入、输出都是一致的输入的是字节码二进制流处理过程是字节码解析执行的等效过程输出的是执行结果8.2 运行时栈帧结构Java虚拟机以方法作为最基本的执行单元“栈帧”是用于支持虚拟机进行方法调用和方法执行背后的数据结构也是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息在编译Java程序源码的时候栈帧中需要多大的局部变量表需要多深的操作数栈就已经被分析计算出来并且写入到方法表的Code属性之中。换言之一个栈帧需要分配多少内存并不会受到程序运行期变量数据的影响而仅仅取决于程序源码和具体的虚拟机实现的栈内存布局形式。8.2.1 局部变量表存放方法参数和方法内部定义的局部变量在在Java程序被编译为Class文件时就在方法的Code属性的max_locals数据项中确定了该方法所需分配的局部变量表的最大容量局部变量表的容量以变量槽为最小单位但没有规定一个变量槽占用的内存空间大小只是很说到每个变量槽都应该能存放一个boolean、byte、char、short、int、float、reference或returnAddress类型的数据允许变量槽的长度可以随着处理器、操作系统或虚拟机实现的不同而发生变化对于64位的数据类型Java虚拟机会以高位对齐的方式为其分配两个连续的变量槽空间索引定位使用局部变量表如果访问的是32位数据类型的变量索引N就代表了使用第N个变量槽如果访问的是64位数据类型的变量则说明会同时使用第N和N1两个变量槽。对于两个相邻的共同存放一个64位数据的两个变量槽虚拟机不允许采用任何方式单独访问其中的某一个局部变量表中的变量槽是可以重用的不过可能会影响垃圾回收回不回收得看有没有覆盖那个槽位注意不同于类变量在准备阶段赋予了初始值如果一个局部变量定义了但没有赋初始值那它是完全不能使用的8.2.2 操作数栈同局部变量表一样操作数栈的最大深度也在编译的时候被写入到Code属性的max_stacks数据项之中当一个方法刚刚开始执行的时候这个方法的操作数栈是空的在方法的执行过程中会有各种字节码指令往操作数栈中写入和提取内容也就是出栈和入栈操作在概念模型中两个不同栈帧作为不同方法的虚拟机栈的元素是完全相互独立的。但是在大多虚拟机的实现里都会进行一些优化处理令两个栈帧出现一部分重叠可以节约空间共用数据减少额外的参数复制传递8.2.3 动态连接每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用持有这个引用是为了支持方法调用过程中的动态连接静态解析符号引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用动态连接另外一部分将在每一次运行期间都转化为直接引用8.2.4 方法返回地址无论采用何种退出方式在方法退出之后都必须返回到最初方法被调用时的位置正常调用完成执行引擎遇到任意一个方法返回的字节码指令这时候可能会有返回值传递给上层的方法调用者调用当前方法的方法称为调用者或者主调方法方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定另外一种退出方式是在方法执行的过程中遇到了异常并且这个异常没有在方法体内得到妥善处理8.2.5 附加信息例如与调试、性能收集相关的信息这部分信息完全取决于具体的虚拟机实现8.3 方法调用方法调用并不等同于方法中的代码被执行方法调用阶段唯一的任务就是确定被调用方法的版本 即调用哪一个方法暂时还未涉及方法内部的具体运行过程Java 在执行一个方法调用时首先会尝试“解析”。如果这个方法在编译时就能确定唯一的版本比如是 static 或 private 方法那就在类加载时直接把调用关系定死这叫“解析”。如果不行比如是一个可以被子类重写的方法那就必须等到程序运行时看看到底是哪个对象在调用这个方法再根据对象的实际类型来决定执行哪个版本这个过程叫“分派”。8.3.1 解析解析指的是在程序真正运行之前JVM就已经百分之百确定了要调用的具体是哪个方法。这个方法的地址在编译和类加载时就被确定下来写死了。是一个静态的过程在程序运行前就完成了。效率高因为不用临时做决定。有1. 静态方法用static修饰。它属于类不属于任何对象。比如Math.random调用它时永远都是那个固定的方法不可能有第二个版本。对应指令invokestatic)2. 私有方法用private修饰。因为它只在自己的类内部可见子类无法继承也无法重写它所以也不可能有第二个版本。对应指令invokespecial3. 构造器init)new一个对象时调用的方法。new哪个类调用的就是哪个类的构造器这是确定无疑的。对应指令invokespecial4. 父类方法用super明确指定了要调用父类的版本JVM不会再有疑问。对应指令invokespecial5. final方法用final修饰的方法。final的意思就是不许重写。所以子类也无法提供新版本负责人也是固定的。8.3.2 分派可能是一个动态的过程需要在程序运行时根据对象的实际类型来确定到底调用哪个方法。这是实现多态的关键。假设公司有一个任务叫接待客户”。今天来的是一位讲英语的客户总指挥就要派擅长英语的员工A去接待。明天来的是讲日语的客户就要派擅长日语的员工B去。总指挥手里的指令是接待客户”但具体派谁去调用哪个方法不能提前定死必须在客户到来时程序运行时根据客户的类型对象的实际类型来临时决定。可能被重写Override的方法就称为虚方法VirtualMethod。静态类型和动态类型以Father son new Son();举例静态类型Static Type也叫“外观类型”就是在代码中声明这个变量时用的类型。在这里Father 就是它的静态类型。这个类型在编译期就已经确定并且不会再改变。无论 son 后来指向了哪个子类对象编译器眼中的 son 永远是 Father。实际类型Actual Type也叫“运行时类型”指的是在程序运行时实际指向内存中的对象的类型。在这里new Son() 创建的是 Son 对象所以 son 的实际类型是 Son。这个类型在运行时才能确定并且可能改变 比如后面可以再写 son new AnotherSon();1. 静态分派所有依赖静态类型来决定方法执行版本的分派动作都称为静态分派。静态分派发生在编译期编译器就像一个“脸盲”它看不见对象真正的样子实际类型它只能看到变量的“标签”静态类型。编译器看到sr.sayHello(man)它分析“嗯变量man的静态类型是Human。”于是它就在三个重载方法中选择了参数为Human的那一个public void sayHello(Human guy)。这个决定在编译成.class文件时就已经被写死了。同理当编译器看到sr.sayHello(woman)时它分析“变量woman的静态类型也是Human。”于是它也选择sayHello(Human guy)这个版本。所以在main方法的两句调用中编译器选择的都是同一个方法。静态分派的最典型应用表现就是方法重载同一个类中方法名相同参数列表不同类型、个数、顺序可能会自动类型转换按照charintlongfloatdouble的顺序转型进行匹配2. 动态分派方法重写Override的多态表现叫做动态分派JVM会检查实际类型是什么根据实际类型选择重写的方法父子类中方法签名完全一样Human man new Man();Human woman new Woman();man.sayHello();woman.sayHello();man new Woman();man.sayHello();输出的是man say hellowoman say hellowoman say hello方法有多态而字段没有多态输出两句都是“I am Son”这是因为Son类在创建的时候首先隐式调用了Father的构造函数而Father构造函数中对showMeTheMoney()的调用是一次虚方法调用实际执行的版本是Son类重写的showMeTheMoney()方法所以输出的是“I am Son”。而这时候虽然父类的money字段已经被初始化成2了但Son类showMeTheMoney()方法中访问的却是子类的money字段这时候结果自然还是0因为它要到子类的构造函数执行时才会被初始化。main()的最后一句通过静态类型访问到了父类中的money输出了2。Father gay new Son();System.out.println(This gay has $ gay.money);​输出I am Son, i have $0I am Son, i have $4This gay has $23. 单分派与多分派方法的接收者与方法的参数统称为方法的宗量单分派根据一个宗量对目标方法进行选择Java语言的动态分派属于单分派类型多分派根据多于一个宗量对目标方法进行选择Java语言的静态分派属于多分派类型。单分派和多分派描述的是Java的工作“阶段”而不是某一行代码的“属性”。Father father new Father();Father son new Son();father.hardChoice(new _360());son.hardChoice(new QQ());father.hardChoice(new _360()); son.hardChoice(new QQ()); 它们都经历了“先静态分派后动态分派”的完整过程。第 1 阶段编译时 (静态多分派)编译器处理这两行代码时都是多分派。对于 father.hardChoice(new _360());宗量1 (接收者静态类型): father 的声明类型是 Father。宗量2 (参数静态类型): new _360() 的类型是 _360。决策: 编译器根据 Father 和 _360 这两个宗量确定了要调用 Father 类里 hardChoice(_360 arg) 这个版本的方法。对于 son.hardChoice(new QQ());宗量1 (接收者静态类型): son 的声明类型是 Father。宗量2 (参数静态类型): new QQ() 的类型是 QQ。决策: 编译器根据 Father 和 QQ 这两个宗量确定了要调用 Father 类里 hardChoice(QQ arg) 这个版本的方法。在这个阶段因为编译器总是同时考虑“接收者的声明类型”和“参数的类型”所以这个阶段是多分派。第 2 阶段运行时 (动态单分派)JVM 实际执行这两行代码时都是单分派。对于 father.hardChoice(new _360());JVM拿到的指令: “去调用 hardChoice(_360) 这个方法”。方法签名已由编译器定死唯一的决策依据 (宗量1): father 变量指向的实际对象是什么类型是 Father 类型。决策: 在 Father 类型中执行 hardChoice(_360) 方法。因为只根据一个宗量对象的实际类型做决策所以这是单分派。对于 son.hardChoice(new QQ());JVM拿到的指令: “去调用 hardChoice(QQ) 这个方法”。方法签名已由编译器定死唯一的决策依据 (宗量1): son 变量指向的实际对象是什么类型是 Son 类型。决策: 检查到 Son 类型重写了 hardChoice(QQ) 方法因此去执行 Son 类的版本。因为只根据一个宗量对象的实际类型做决策所以这也是单分派。​4. 虚拟机如何实现动态分派建立一个虚方法表像查数组一样快不需要搜索效率高虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的都指向父类的实现入口。如果子类中重写了这个方法子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。比喻可以把虚方法表想象成每个类的一张“方法快捷拨号清单”。1.Object类有一张清单快捷键1toString()快捷键2equals()2.Father类继承了Object,它的清单是快捷键1toString()指向Object的快捷键2equals()指向Object的快捷键3hardChoice(QQ)指向Father自己的快捷键4hardChoice(_360)指向Father自己的3.Son类继承了Father,并且重写了方法它的清单是快捷键1toString()指向Object的快捷键2equals()指向Object的快捷键3hardChoice(QQ)指向Son自己的覆盖了Father的快捷键4hardChoice(_360)指向Son自己的覆盖了Father的当JVM要执行son.hardChoice(new QQ())时它已经通过编译器的信息知道了要按“快捷键3”。它只需要1. 查看son对象的实际类型是Son。2. 找到Son类的“快捷拨号清单”(vtable)。3. 直接按“快捷键3”。
延伸阅读

更多相关文章

2026/11/3 19:32:21

空间智能终端正在从单点设备走向系统协同,行业竞争逻辑已经变了

过去几年,空间智能终端的竞争集中在硬件本身:摄像头像素、门锁识别速度、面板响应时间、传感器精度。但当一个项目从几百台设备扩展到几万台,从单一住宅延伸到公寓、酒店、商业、园区多业态共存时,真正的瓶颈不再是某个终端够不够…

2026/11/3 19:32:21

05 - 端口与实时画面(训练 / 评估)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 19:32:21

web打印插件四选一:从浏览器打印到可视化模板设计器

后台系统做到一定阶段,打印一定会变成最脏的活。 业务最初说"能打印就行",上线三个月后需求变成:表头要在每页重复、这张单要套打、批量打 200 张、不要弹框、能不能让业务自己改模板。等你把这些都补上,代码里已经堆了…

2026/11/3 20:17:26

基于Django与微信小程序的毕业设计导师双向选择系统

1. 项目概述:基于Django与微信小程序的毕业设计导师双向选择系统这个毕业设计选题瞄准了高校教学管理中的一个实际痛点——每年毕业季,学生与导师之间的双向选择过程往往效率低下、信息不对称。传统方式依赖纸质表格或简单线上表单,学生无法全…

2026/11/3 20:17:26

企业邮件安全:从边界防御到智能动态感知架构

1. 企业邮件安全的现状与挑战过去十年间,我参与过数十家企业邮件系统的安全架构设计与升级。最深刻的体会是:传统的边界防御策略正在失效。上周某金融客户的案例就很典型——他们部署了顶级防火墙、反垃圾邮件网关和沙箱系统,但依然被钓鱼邮件…

2026/11/3 20:17:26

GPT-4与ChatGPT辨析:LLM、Transformer与Token实践

简介:《Developing Apps with GPT-4 and ChatGPT》是一份 O’Reilly 出版的英文电子书 PDF,面向希望把大模型能力落地到实际产品的开发者、技术负责人以及生成式 AI 爱好者,重点讨论如何基于 GPT-4 与 ChatGPT 构建智能聊天机器人、内容生成器…

2026/11/3 20:17:26

规格驱动开发:从提示词到可执行代码的实践

1. 项目概述:当提示词遇见规格化编程三年前我第一次尝试用自然语言生成代码时,还需要在GPT对话框里输入大段注释般的描述。如今,在SpecKit工具链的支持下,我只需输入"创建用户注册API,需要邮箱验证和密码强度校验…

2026/11/3 20:17:26

规格驱动开发:从自然语言到可执行代码的自动化实践

1. 项目概述:当提示词遇见规格化编程三年前我第一次尝试用自然语言生成代码时,面对满屏似是而非的函数和混乱的逻辑结构,不得不花费数小时手动修正。而今天在SpecKit工具链的帮助下,同样的需求通过结构化提示词驱动,生…

2026/11/3 20:12:26

Vibe Coding:提升开发效率的心流编程方法论

1. 什么是Vibe Coding?第一次听到Vibe Coding这个词是在一个开发者社区,当时看到有人分享用这种方式完成了一个人月的工作量。简单来说,Vibe Coding是一种强调开发节奏和沉浸感的编程方法论,核心在于通过特定的工具链和工作流&…

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

STM32实战:基于MAX14431数字隔离器的SPI隔离通信方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

工业SPI隔离通信设计:MAX14431BAEE+与STM32L4A6RG实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 1:04:48

STM32+NRF24L01无线抢答器:硬件设计、通信协议与低功耗实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/11/3 7:47:06

面向复杂光照与角度的宠物面部识别鲁棒性增强策略

在智慧养宠时代,基于视觉的宠物身份识别技术正逐步从实验室走向家庭应用。然而,与配合度高的人类不同,宠物是典型的“非合作生物识别对象”——它们不会主动配合摄像头调整姿态,更无法控制光照环境。实际应用中,我们常…

2026/11/3 14:24:53

SSM电子书店源码全解析:结构、原理、部署与排错实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑