动手实践:从栈帧看字节码是如何在 JVM 中进行流转的
Java全能学习+面试指南:https://www.javaxiaobear.cn/
前面我们提到,类的初始化发生在类加载阶段,那对象都有哪些创建方式呢?除了我们常用的 new,还有下面这些方式:
- 使用 Class 的 newInstance 方法。
- 使用 Constructor 类的 newInstance 方法。
- 反序列化。
- 使用 Object 的 clone 方法。
其中,后面两种方式没有调用到构造函数。
当虚拟机遇到一条 new 指令时,首先会检查这个指令的参数能否在常量池中定位一个符号引用。然后检查这个符号引用的类字节码是否加载、解析和初始化。如果没有,将执行对应的类加载过程。
拿我们上面的代码来说,执行 A 代码,在调用 private B b = new B() 时,就会触发 B 类的加载。

让我们结合上图回顾一下前面章节的内容。A 和 B 会被加载到元空间的方法区,进入 main 方法后,将会交给执行引擎执行。这个执行过程是在栈上完成的,其中有几个重要的区域,包括虚拟机栈、程序计数器等。接下来我们详细看一下虚拟机栈上的执行过程。
查看字节码
命令行查看字节码
使用下面的命令编译源代码 A.java。如果你用的是 Idea,可以直接将参数追加在 VM options 里面。
javac -g:lines -g:vars A.java
这将强制生成 LineNumberTable 和 LocalVariableTable。
然后使用 javap 命令查看 A 和 B 的字节码。
javap -p -v A
javap -p -v B
这个命令,不仅会输出行号、本地变量表信息、反编译汇编代码,还会输出当前类用到的常量池等信息。由于内容很长,这里就不具体展示了,你可以使用上面的命令实际操作一下就可以了。
注意 javap 中的如下字样。
<1>
1: invokespecial #1 // Method java/lang/Object."<init>":()V
可以看到对象的初始化,首先是调用了 Object 类的初始化方法。注意这里是 <init> 而不是 <cinit>。
<2>
#2 = Fieldref #6.#27 // B.a:I
它其实直接拼接了 #13 和 #14 的内容。
#6 = Class #29 // B
#27 = NameAndType #8:#9 // a:I
...
#8 = Utf8 a
#9 = Utf8 I
<3>
你会注意到 :I 这样特殊的字符。它们也是有意义的,如果你经常使用 jmap 这种命令,应该不会陌生。大体包括:
- B 基本类型 byte
- C 基本类型 char
- D 基本类型 double
- F 基本类型 float
- I 基本类型 int
- J 基本类型 long
- S 基本类型 short
- Z 基本类型 boolean
- V 特殊类型 void
- L 对象类型,以分号结尾,如 Ljava/lang/Object;
- [Ljava/lang/String; 数组类型,每一位使用一个前置的"["字符来描述
我们注意到 code 区域,有非常多的二进制指令。如果你接触过汇编语言,会发现它们之间其实有一定的相似性。但这些二进制指令,并不是操作系统能够认识的,它们是提供给 JVM 运行的源材料。
可视化查看字节码
接下来,我们就可以使用更加直观的工具 jclasslib,来查看字节码中的具体内容了。
我们以 B.class 文件为例,来查看它的内容。
<1>
首先,我们能够看到 Constant Pool(常量池),这些内容,就存放于我们的 Metaspace 区域,属于非堆。

常量池包含 .class 文件常量池、运行时常量池、String 常量池等部分,大多是一些静态内容。
<2>
接下来,可以看到两个默认的 <init> 和 <cinit> 方法。以下截图是 test 方法的 code 区域,比命令行版的更加直观。

<3>
继续往下看,我们看到了 LocalVariableTable 的三个变量。其中,slot 0 指向的是 this 关键字。该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。如果没有这些信息,那么在 IDE 中引用这个方法时,将无法获取到方法名,取而代之的则是 arg0 这样的变量名。

本地变量表的 slot 是可以复用的。注意一个有意思的地方,index 的最大值为 3,证明了本地变量表同时最多能够存放 4 个变量。
另外,我们观察到还有 LineNumberTable 等选项。该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系,有了这些信息,在 debug 时,就能够获取到发生异常的源代码行号。
test 函数执行过程
Code 区域介绍
test 函数同时使用了成员变量 a、静态变量 C,以及输入参数 num。我们此时说的函数执行,内存其实就是在虚拟机栈上分配的。下面这些内容,就是 test 方法的字节码。
public long test(long);descriptor: (J)Jflags: ACC_PUBLICCode:stack=4, locals=5, args_size=20: aload_01: getfield #2 // Field a:I4: i2l5: lload_16: ladd7: getstatic #3 // Field C:J10: ladd11: lstore_312: lload_313: lreturnLineNumberTable:line 13: 0line 14: 12LocalVariableTable:Start Length Slot Name Signature0 14 0 this LB;0 14 1 num J12 2 3 ret J
我们介绍一下比较重要的 3 三个数值。
<1>
首先,注意 stack 字样,它此时的数值为 4,表明了 test 方法的最大操作数栈深度为 4。JVM 运行时,会根据这个数值,来分配栈帧中操作栈的深度。
<2>
相对应的,locals 变量存储了局部变量的存储空间。它的单位是 Slot(槽),可以被重用。其中存放的内容,包括:
- this
- 方法参数
- 异常处理器的参数
- 方法体中定义的局部变量
<3>
args_size 就比较好理解。它指的是方法的参数个数,因为每个方法都有一个隐藏参数 this,所以这里的数字是 2。
字节码执行过程
我们稍微回顾一下 JVM 运行时的相关内容。main 线程会拥有两个主要的运行时区域:Java 虚拟机栈和程序计数器。其中,虚拟机栈中的每一项内容叫作栈帧,栈帧中包含四项内容:局部变量报表、操作数栈、动态链接和完成出口。
我们的字节码指令,就是靠操作这些数据结构运行的。下面我们看一下具体的字节码指令。

(1)0: aload_0
把第 1 个引用型局部变量推到操作数栈,这里的意思是把 this 装载到了操作数栈中。
对于 static 方法,aload_0 表示对方法的第一个参数的操作。

(2)1: getfield #2
将栈顶的指定的对象的第 2 个实例域(Field)的值,压入栈顶。#2 就是指的我们的成员变量 a。
#2 = Fieldref #6.#27 // B.a:I
...
#6 = Class #29 // B
#27 = NameAndType #8:#9 // a:I

(3)i2l
将栈顶 int 类型的数据转化为 long 类型,这里就涉及我们的隐式类型转换了。图中的信息没有变动,不再详解介绍。
(4)lload_1
将第一个局部变量入栈。也就是我们的参数 num。这里的 l 表示 long,同样用于局部变量装载。你会看到这个位置的局部变量,一开始就已经有值了。

(5)ladd
把栈顶两个 long 型数值出栈后相加,并将结果入栈。

(6)getstatic #3
根据偏移获取静态属性的值,并把这个值 push 到操作数栈上。

(7)ladd
再次执行 ladd。

(8)lstore_3
把栈顶 long 型数值存入第 4 个局部变量。
还记得我们上面的图么?slot 为 4,索引为 3 的就是 ret 变量。

(9)lload_3
正好与上面相反。上面是变量存入,我们现在要做的,就是把这个变量 ret,压入虚拟机栈中。

(10)lreturn
从当前方法返回 long。
到此为止,我们的函数就完成了相加动作,执行成功了。JVM 为我们提供了非常丰富的字节码指令。详细的字节码指令列表,可以参考以下网址:
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html
注意点
注意上面的第 8 步,我们首先把变量存放到了变量报表,然后又拿出这个值,把它入栈。为什么会有这种多此一举的操作?原因就在于我们定义了 ret 变量。JVM 不知道后面还会不会用到这个变量,所以只好傻瓜式的顺序执行。
为了看到这些差异。大家可以把我们的程序稍微改动一下,直接返回这个值。
public long test(long num) {return this.a + num + C;
}
再次看下,对应的字节码指令是不是简单了很多?
0: aload_0
1: getfield #2 // Field a:I
4: i2l
5: lload_1
6: ladd
7: getstatic #3 // Field C:J
10: ladd
11: lreturn
那我们以后编写程序时,是不是要尽量少的定义成员变量?
这是没有必要的。栈的操作复杂度是 O(1),对我们的程序性能几乎没有影响。平常的代码编写,还是以可读性作为首要任务。
小结
我们学会了使用 javap 和 jclasslib 两个工具。平常工作中,掌握第一个就够了,后者主要为我们提供更加直观的展示。
我们从实际分析一段代码开始,详细介绍了几个字节码指令对程序计数器、局部变量表、操作数栈等内容的影响,初步接触了 Java 的字节码文件格式。
希望你能够建立起一个运行时的脉络,在看到相关的 opcode 时,能够举一反三的思考背后对这些数据结构的操作。这样理解的字节码指令,根本不会忘。
你还可以尝试着对 A 类的代码进行分析,我们这里先留下一个悬念。
相关文章:
动手实践:从栈帧看字节码是如何在 JVM 中进行流转的
Java全能学习面试指南:https://www.javaxiaobear.cn/ 前面我们提到,类的初始化发生在类加载阶段,那对象都有哪些创建方式呢?除了我们常用的 new,还有下面这些方式: 使用 Class 的 newInstance 方法。使用…...
PEX装机
目录 一、PXE是什么? 二、PXE的组件: vsftpd/httpd/nfs tftp dhcp 三、配置vsftpd 四、配置tftp 1.安装tftp-server 2.启动tftp 五、准备pxelinx.0文件、引导文件、内核文件 1.准备pxelinux.0文件 2.准备引导文件、内核文件 六、配置dhcp …...
异地远程访问内网BUG管理系统【Cpolar内网穿透】
文章目录 前言1. 本地安装配置BUG管理系统2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射本地服务3. 测试公网远程访问4. 配置固定二级子域名4.1 保留一个二级子域名5.1 配置二级子域名6. 使用固定二级子域名远程 前言 BUG管理软件,作为软件测试工程师的必备工具之一。在…...
论文笔记:一分类及其在大数据中的潜在应用综述
0 概述 论文:A literature review on one‑class classification and its potential applications in big data 发表:Journal of Big Data 在严重不平衡的数据集中,使用传统的二分类或多分类通常会导致对具有大量实例的类的偏见。在这种情况…...
下单时如何保证数据一致性?
原创 哪吒 哪吒编程 2023-09-07 08:03 发表于辽宁 收录于合集#Redis11个 (给哪吒编程加星标,提高Java技能) 大家好,我是哪吒。 在前几篇文章中,提到了Redis实现排行榜、Redis数据缓存策略,让我们对Redis…...
【C++ Core Guidelines解析】深入理解现代C++的特性和原理
文章目录 👨⚖️《C Core Guidelines解析》的主要观点👨🏫《C Core Guidelines解析》的主要内容👨💻作者介绍 🌸🌸🌸🌷🌷🌷💐&a…...
Go语言高阶:Reflection反射与Files操作 详细示例教程
目录标题 一、Reflection反射1. What is reflection? 什么是反射2. Inspect a variable and find its type 检查变量并找到它的类型3. Reflect.Type and reflect.Value 反射类型和值4. Reflect.Kind 查看底层种类5. NumField() and Field() methods 字段数量和索引值方法6. In…...
谷歌seo技术流
很多外贸企业和独立站都想从Google获得免费的流量,也就是SEO流量,但是在做SEO的过程中,总会面临这样或那样的问题。米贸搜谷歌推广将这些问题总结如下: 既然SEO看起来似乎很难,但还是有很多电商公司愿意投资SEO&#x…...
ReactiveUI MVVM框架(1)-Collections
ReactiveUI MVVM框架(1)-Collections ReactiveUI使用动态数据(DynamicData)用于集合的操作。 当对动态数据集合进行更改时,会产生更改通知,通知表示为ChangeSet,里面包含了更改信息࿰…...
【微服务】五. Nacos服务注册
Nacos服务注册 5.1 Nacos服务分级存储模型Nacos服务分级存储模型:服务集群属性:总结: 5.2 根据集群负载均衡总结 5.3 Nacos服务实例的权重设置总结: 5.6 环境隔离namespace总结 5.7 Nacos和Eureka的对比总结 5.1 Nacos服务分级存储…...
Lnmp架构-Redis
网站:www.redis.cn redis 部署 make的时候需要gcc和make 如果在纯净的环境下需要执行此命令 [rootserver3 redis-6.2.4]# yum install make gcc -y 注释一下这几行 vim /etc/redis/6739.conf 2.Redis主从复制 设置 11 是master 12 13 是slave 在12 上 其他节…...
Python 二进制数据处理与转换
不得不说,Python能火是有原因的,物联网开发中常用的数据处理方式,Python都有内置的函数或方法,相当方便,官方文档见二进制序列类型,下面是一些示例代码 string Hello World! # 字符串转二进制数据 data …...
【LeetCode】297.二叉树的序列化与反序列化
题目 序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。 请设计一个算法来实现二叉树的序列化与反序列化…...
Java HashSet
HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。 HashSet 允许有 null 值。 HashSet 是无序的,即不会记录插入的顺序。 HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必…...
在iPhone上构建自定义数据采集完整指南
在iPhone上构建自定义数据采集工具可以帮助我们更好地满足特定需求,提高数据采集的灵活性和准确性。本文将为您提供一份完整的指南和示例代码,教您如何在iPhone上构建自定义数据采集工具。 自定义数据采集工具的核心组件 a、数据模型 数据模型是数据采…...
Android MediaRecorder录音
1. 简介 在android中录制音频有两种方式,MediaRecorder和AudioRecord。两者的区别如下: MediaRecorder 简单方便,不需要理会中间录制过程,结束录制后可以直接得到音频文件进行播放;录制的音频文件是经过压缩的&#…...
软件提示vcruntime140_1.dll丢失的解决方法,以及丢失的原因总结
在运行某些程序时,可能会出现“vcruntime140_1.dll 丢失”的错误提示。这是因为 vcruntime140_1.dll 是 Visual C Redistributable 的一部分,它通常被安装在 Windows 操作系统上。如果该文件丢失或无法找到,可能会导致程序无法正常运行。在我…...
Datax抽取mysql的bit类型数据
背景:使用datax抽取mysql的一张表,里面有两个bit类型的字段,抽取出来显示如下: 需要在抽取reader里面进行处理配置 最终生成的datax的json文件reader的配置会转换为具体的数值 最终查询效果:...
git 后悔药
前言 自上而下,撤销可以分为从远程库撤销,从本地库撤销,从暂存库撤销。 例子:代码已经提交了三个记录到远程库,分别对应了记录1,内容1,记录2,内容2,记录3,内…...
vue-cli搭建一个新项目及基础配置
vue-cli搭建一个新项目及基础配置 一、安装步骤二、main.js配置三、router下的index.js 一、安装步骤 1.安装node环境:下载地址:Node.js 2.安装脚手架:npm install -g vue/cli 3.创建vue项目:vue create 项目名 4.进入项目&…...
uniapp 对接腾讯云IM群组成员管理(增删改查)
UniApp 实战:腾讯云IM群组成员管理(增删改查) 一、前言 在社交类App开发中,群组成员管理是核心功能之一。本文将基于UniApp框架,结合腾讯云IM SDK,详细讲解如何实现群组成员的增删改查全流程。 权限校验…...
Cursor实现用excel数据填充word模版的方法
cursor主页:https://www.cursor.com/ 任务目标:把excel格式的数据里的单元格,按照某一个固定模版填充到word中 文章目录 注意事项逐步生成程序1. 确定格式2. 调试程序 注意事项 直接给一个excel文件和最终呈现的word文件的示例,…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...
python报错No module named ‘tensorflow.keras‘
是由于不同版本的tensorflow下的keras所在的路径不同,结合所安装的tensorflow的目录结构修改from语句即可。 原语句: from tensorflow.keras.layers import Conv1D, MaxPooling1D, LSTM, Dense 修改后: from tensorflow.python.keras.lay…...
React---day11
14.4 react-redux第三方库 提供connect、thunk之类的函数 以获取一个banner数据为例子 store: 我们在使用异步的时候理应是要使用中间件的,但是configureStore 已经自动集成了 redux-thunk,注意action里面要返回函数 import { configureS…...
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join
纯 Java 项目(非 SpringBoot)集成 Mybatis-Plus 和 Mybatis-Plus-Join 1、依赖1.1、依赖版本1.2、pom.xml 2、代码2.1、SqlSession 构造器2.2、MybatisPlus代码生成器2.3、获取 config.yml 配置2.3.1、config.yml2.3.2、项目配置类 2.4、ftl 模板2.4.1、…...
【技巧】dify前端源代码修改第一弹-增加tab页
回到目录 【技巧】dify前端源代码修改第一弹-增加tab页 尝试修改dify的前端源代码,在知识库增加一个tab页"HELLO WORLD",完成后的效果如下 [gif01] 1. 前端代码进入调试模式 参考 【部署】win10的wsl环境下启动dify的web前端服务 启动调试…...
深入浅出JavaScript中的ArrayBuffer:二进制数据的“瑞士军刀”
深入浅出JavaScript中的ArrayBuffer:二进制数据的“瑞士军刀” 在JavaScript中,我们经常需要处理文本、数组、对象等数据类型。但当我们需要处理文件上传、图像处理、网络通信等场景时,单纯依赖字符串或数组就显得力不从心了。这时ÿ…...
HarmonyOS-ArkUI 自定义弹窗
自定义弹窗 自定义弹窗是界面开发中最为常用的一种弹窗写法。在自定义弹窗中, 布局样式完全由您决定,非常灵活。通常会被封装成工具类,以使得APP中所有弹窗具备相同的设计风格。 自定义弹窗具备的能力有 打开弹窗自定义布局,以…...
云原生技术驱动 IT 架构现代化转型:企业实践与落地策略全解
📝个人主页🌹:慌ZHANG-CSDN博客 🌹🌹期待您的关注 🌹🌹 一、背景:IT 架构演进的战略拐点 过去十年,企业 IT 架构经历了从传统集中式架构到分布式架构的转型。进入云计算…...
