图文详解 Java 字节码,想不懂都难!

本文探讨了JVM的数据类型、基于栈的架构及字节码指令集,通过示例代码展示了字节码的生成过程,解释了方法调用、实例创建与初始化的字节码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM 数据类型

Java是静态类型的,它会影响字节码指令的设计,这样指令就会期望自己对特定类型的值进行操作。例如,就会有好几个add指令用于两个数字相加:iadd、ladd、fadd、dadd。他们期望类型的操作数分别是int、long、float和double。大多数字节码都有这样的特性,它具有不同形式的相同功能,这取决于操作数类型。

JVM定义的数据类型包括:

基本类型:

数值类型: byte (8位), short (16位), int (32位), long (64-bit位), char (16位无符号Unicode), float(32-bit IEEE 754 单精度浮点型), double (64-bit IEEE 754 双精度浮点型)

布尔类型

指针类型: 指令指针。

 

引用类型:

数组

接口

在字节码中布尔类型的支持是受限的。举例来说,没有结构能直接操作布尔值。布尔值被替换转换成 int 是通过编译器来进行的,并且最终还是被转换成 int 结构。

Java 开发者应该熟悉所有上面的类型,除了 returnAddress,它没有等价的编程语言类型。

基于栈的架构

字节码指令集的简单性很大程度上是由于 Sun 设计了基于堆栈的 VM 架构,而不是基于寄存器架构。有各种各样的进程使用基于JVM 的内存组件, 但基本上只有 JVM 堆需要详细检查字节码指令:

PC寄存器:对于Java程序中每个正在运行的线程,都有一个PC寄存器保存着当前执行的指令地址。

JVM 栈:对于每个线程,都会分配一个栈,其中存放本地变量、方法参数和返回值。下面是一个显示3个线程的堆栈示例。

:所有线程共享的内存和存储对象(类实例和数组)。对象回收是由垃圾收集器管理的。

方法区:对于每个已加载的类,它储存方法的代码和一个符号表(例如对字段或方法的引用)和常量池。

JVM堆栈是由帧组成的,当方法被调用时,每个帧都被推到堆栈上,当方法完成时从堆栈中弹出(通过正常返回或抛出异常)。每一帧还包括:

  1. 本地变量数组,索引从0到它的长度-1。长度是由编译器计算的。一个局部变量可以保存任何类型的值,long和double类型的值占用两个局部变量。
  2. 用来存储中间值的栈,它存储指令的操作数,或者方法调用的参数。

字节码探索

关于JVM内部的看法,我们能够从示例代码中看到一些被生成的基本字节码例子。Java类文件中的每个方法都有代码段,这些代码段包含了一系列的指令,格式如下:

opcode (1 byte)      operand1 (optional)      operand2 (optional)      ...

这个指令是由一个一字节的opcode和零个或若干个operand组成的,这个operand包含了要被操作的数据。

在当前执行方法的栈帧里,一条指令可以将值在操作栈中入栈或出栈,可以在本地变量数组中悄悄地加载或者存储值。让我们来看一个例子:

为了打印被编译的类中的结果字节码(假设在Test.class文件中),我们运行javap工具:

我们可以得到如下结果:

我们可以看到main方法的方法声明,descriptor说明这个方法的参数是一个字符串数组([Ljava/lang/String; ),而且返回类型是void(V)。下面的flags这行说明该方法是公开的(ACC_PUBLIC)和静态的 (ACC_STATIC)。

Code属性是最重要的部分,它包含了这个方法的一系列指令和信息,这些信息包含了操作栈的最大深度(本例中是2)和在这个方法的这一帧中被分配的本地变量的数量(本例中是4)。所有的本地变量在上面的指令中都提到了,除了第一个变量(索引为0),这个变量保存的是args参数。其他三个本地变量就相当于源码中的a,b和c。

从地址0到8的指令将执行以下操作:

iconst_1:将整形常量1放入操作数栈。

istore_1:在索引为1的位置将第一个操作数出栈(一个int值)并且将其存进本地变量,相当于变量a。

const_2:将整形常量2放入操作数栈。

istore_2:在索引为2的位置将第一个操作数出栈并且将其存进本地变量,相当于变量b。

iload_1:从索引1的本地变量中加载一个int值,放入操作数栈。

iload_2:从索引2的本地变量中加载一个int值,放入操作数栈。

iadd:把操作数栈中的前两个int值出栈并相加,将相加的结果放入操作数栈。

istore_3:在索引为3的位置将第一个操作数出栈并且将其存进本地变量,相当于变量c。

return:从这个void方法中返回。上述指令只包含操作码,由JVM来精确执行。

方法调用

上面的示例只有一个方法,即 main 方法。假如我们需要对变量 c 进行更复杂的计算,这些复杂的计算写在新方法 calc 中:

看看生成的字节码:

main 方法代码唯一的不同在于用 invokestatic 指令代替了 iadd 指令,invokestatic 指令用于调用静态方法 calc。注意,关键在于操作数栈中传递给 calc 方法的两个参数。也就是说,调用方法需要按正确的顺序为被调用方法准备好所有参数,交依次推入操作数栈。iinvokestatic(还有后面提到的其它类似的调用指令)随后会从栈中取出这些参数,然后为被调用方法创建一个新的环境,将参数作为局域变量置于其中。

我们也注意到invokestatic指令在地址上看占据了3字节,由6跳转到9。不像其余指令那样那么远,这是因为invokestatic指令包含了两个额外的字节来构造要调用的方法的引用(除了opcode外)。这引用由javap显示为#2,是一个引用calc方法的符号,解析于从前面描述的常量池中。

其它的新信息显然是calc方法本身的代码。它首先将第一个整数参数加载到操作数堆栈上(iload_0)。下一条指令,i2d,通过应用扩展转换将其转换为double类型。由此产生的double类型取代了操作数堆栈的顶部。

再下一条指令将一个double类型常量2.0d(从常量池中取出)推到操作数堆栈上。然后静态方法Math.pow调用目前为止准备好的两个操作数值(第一个参数是calc和常量2.0d)。当Math.pow方法返回时,他的结果将会被存储在其调用程序的操作数堆栈上。在下面说明。

同样的程序应用于计算Math.pow(b,2):

下一条指令,dadd,会将栈顶的两个中间结果出栈,将它们相加,并将所得之和推入栈顶。最后,invokestatic 对这个和值调用 Math.sqrt,将结果从 double(双精度浮点型) 窄化转换(d2i)成 int(整型)。整型结果会返回到 main 方法中, 并在这里保存到 c(istore_3)。

创建实例

现在修改这个示例,加入 Point 类来封装 XY 坐标。

 

编译后的 main 方法的字体码如下:

这里引入了 new、dup 和 invokespecial 几个新指令。new 指令与编程语言中的 new 运算符类似,它根据传入的操作数所指定类型来创建对象(这是对 Point 类的符号引用)。对象的内存是在堆上分配,对象引用则是被推入到操作数栈上。

dup指令会复制顶部操作数的栈值,这意味着现在我们在栈顶部有两个指向Point对象的引用。接下来的三条指令将构造函数的参数(用于初始化对象)压入操作数堆栈中,然后调用与构造函数对应的特殊初始化方法。下一个方法中x和y字段将被初始化。该方法完成之后,前三个操作数的栈值将被销毁,剩下的就是已创建对象的原始引用(到目前为止,已成功完成初始化了)。

接下来,astore_1将该Point引用出栈,并将其赋值到索引1所保存的本地变量(astore_1中的a表明这是一个引用值).

通用的过程会被重复执行以创建并初始化第二个Point实例,此实例会被赋值给变量b。

最后一步是将本地变量中的两个Point对象的引用加载到索引1和2中(分别使用aload_1和aload_2),并使用invokevirtual调用area方法,该方法会根据实际的类型来调用适当的方法来完成分发。

例如,如果变量a包含一个扩展自Point类的SpecialPoint实例,并且该子类重写了area方法,则重写后的方法会被调用。在这种情况下,并不存在子类,因此仅有area方法是可用的。

请注意,即使area方法接受单参数,堆栈顶部也有两个Point的引用。第一个(pointA,来自变量a)实际上是调用该方法的实例(在编程语言中被称为this),对area方法来说,它将被传递到新栈帧的第一个局部变量中。另一个操作数(pointB)是area方法的参数。

基于数据挖掘的音乐推荐系统设计与实现 需要一个代码说明,不需要论文 采用python语言,django框架,mysql数据库开发 编程环境:pycharm,mysql8.0 系统分为前台+后台模式开发 网站前台: 用户注册, 登录 搜索音乐,音乐欣赏(可以在线进行播放) 用户登陆时选择相关感兴趣的音乐风格 音乐收藏 音乐推荐算法:(重点) 本课题需要大量用户行为(如播放记录、收藏列表)、音乐特征(如音频特征、歌曲元数据)等数据 (1)根据用户之间相似性或关联性,给一个用户推荐与其相似或有关联的其他用户所感兴趣的音乐; (2)根据音乐之间的相似性或关联性,给一个用户推荐与其感兴趣的音乐相似或有关联的其他音乐。 基于用户的推荐和基于物品的推荐 其中基于用户的推荐是基于用户的相似度找出相似相似用户,然后向目标用户推荐其相似用户喜欢的东西(和你类似的人也喜欢**东西); 而基于物品的推荐是基于物品的相似度找出相似的物品做推荐(喜欢该音乐的人还喜欢了**音乐); 管理员 管理员信息管理 注册用户管理,审核 音乐爬虫(爬虫方式爬取网站音乐数据) 音乐信息管理(上传歌曲MP3,以便前台播放) 音乐收藏管理 用户 用户资料修改 我的音乐收藏 完整前后端源码,部署后可正常运行! 环境说明 开发语言:python后端 python版本:3.7 数据库:mysql 5.7+ 数据库工具:Navicat11+ 开发软件:pycharm
MPU6050是一款广泛应用在无人机、机器人和运动设备中的六轴姿态传感器,它集成了三轴陀螺仪和三轴加速度计。这款传感器能够实时监测并提供设备的角速度和线性加速度数据,对于理解物体的动态运动状态至关重要。在Arduino平台上,通过特定的库文件可以方便地与MPU6050进行通信,获取并解析传感器数据。 `MPU6050.cpp`和`MPU6050.h`是Arduino库的关键组成部分。`MPU6050.h`是头文件,包含了定义传感器接口和函数声明。它定义了类`MPU6050`,该类包含了初始化传感器、读取数据等方法。例如,`begin()`函数用于设置传感器的工作模式和I2C地址,`getAcceleration()`和`getGyroscope()`则分别用于获取加速度和角速度数据。 在Arduino项目中,首先需要包含`MPU6050.h`头文件,然后创建`MPU6050`对象,并调用`begin()`函数初始化传感器。之后,可以通过循环调用`getAcceleration()`和`getGyroscope()`来不断更新传感器读数。为了处理这些原始数据,通常还需要进行校准和滤波,以消除噪声和漂移。 I2C通信协议是MPU6050与Arduino交互的基础,它是一种低引脚数的串行通信协议,允许多个设备共享一对数据线。Arduino板上的Wire库提供了I2C通信的底层支持,使得用户无需深入了解通信细节,就能方便地与MPU6050交互。 MPU6050传感器的数据包括加速度(X、Y、Z轴)和角速度(同样为X、Y、Z轴)。加速度数据可以用来计算物体的静态位置和动态运动,而角速度数据则能反映物体转动的速度。结合这两个数据,可以进一步计算出物体的姿态(如角度和角速度变化)。 在嵌入式开发领域,特别是使用STM32微控制器时,也可以找到类似的库来驱动MPU6050。STM32通常具有更强大的处理能力和更多的GPIO口,可以实现更复杂的控制算法。然而,基本的传感器操作流程和数据处理原理与Arduino平台相似。 在实际应用中,除了基本的传感器读取,还可能涉及到温度补偿、低功耗模式设置、DMP(数字运动处理器)功能的利用等高级特性。DMP可以帮助处理传感器数据,实现更高级的运动估计,减轻主控制器的计算负担。 MPU6050是一个强大的六轴传感器,广泛应用于各种需要实时运动追踪的项目中。通过 Arduino 或 STM32 的库文件,开发者可以轻松地与传感器交互,获取并处理数据,实现各种创新应用。博客和其他开源资源是学习和解决问题的重要途径,通过这些资源,开发者可以获得关于MPU6050的详细信息和实践指南
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值