前言:
字节码作为JAVA跨平台的主要原因,熟练的掌握JAVA字节码文件的组成可以帮助我们解决项目的各种问题,并且在面试中,关于字节码部分的内容却是一大考点和难点,因此我们在这里穿插讲解一下字节码文件的组成。
目录
前言:
1.如何打开字节码文件?
2.字节码文件的组成:
3.简单阅读字节码
总结:
1.如何打开字节码文件?
字节码文件中保存了原代码编译后的内容,以二进制的形式进行存储。因此我们是无法用记事本这种普通的阅读软件打开的。
因此我们要使用专业的字节码阅读工具进行。好在IDEA中就有对应的插件,我们只需要点击下载就可以。
jclasslib Bytecode Viewer
然后通过这个插件,我们就可以便捷的查看一个字节码文件:
我们来逐一解释一下这些部分:
1.基础信息:魔数,字节码文件对应的JAVA版本号,访问标识符 ,父类和接口。
2.常量信息:保存了字符串常量,类或接口名,字段名。主要在字节码指令中使用。
3.字段:当前类或接口声明的字段信息。
4.方法:当前类或接口声明的方法信息字节码指令。
5.属性 :类的属性,比如源码的文件名,内部类的列表等。
通过这款插件,我们就可以看到上图的字节码:
2.字节码文件的组成:
ava字节码文件(通常以.class
扩展名结尾)是Java源代码编译后的产物。它包含了Java程序在JVM上执行所需的指令和信息。字节码文件的组成结构非常标准化,按照特定的格式排列,主要由以下部分组成:
-
魔数(Magic Number):
- 每个字节码文件的前4个字节都是固定的魔数
0xCAFEBABE
,用于标识这是一个Java字节码文件。
- 每个字节码文件的前4个字节都是固定的魔数
-
版本信息:
- 紧接着魔数的是版本信息,包括次版本号(Minor Version)和主版本号(Major Version),用于表示编译该字节码的Java编译器版本。
-
常量池(Constant Pool):
- 常量池紧随版本信息之后,是.class文件中资源的集合,包括各种文字字符串、类或接口名、字段名和其他常量。
- 它是一个表结构,存储了类中所有的符号引用,这些引用将在类加载阶段被解析。
-
访问标志(Access Flags):
- 用于描述类或接口的访问权限(如public、private、protected)和属性(如abstract、final、interface等)。
-
此类索引、父类索引和接口索引集合:
- 这些索引值指向常量池中的项,分别代表当前类、超类和实现的接口的符号引用。
-
字段表(Fields Table):
- 包含类或接口中声明的所有字段,每个字段都有其对应的属性集合,如名称、类型、访问标志等。
-
方法表(Methods Table):
- 列出了类中的所有方法,包括方法的名称、返回类型、参数类型、访问标志以及方法的字节码指令。
-
属性表(Attributes Table):
- 属性表为字段表、方法表提供附加信息,如异常表、行号表、局部变量表等。每个属性都有自己的结构定义,例如,Code属性就包含了Java方法的JVM指令、操作数栈、局部变量表等信息。
-
接口表(Interfaces Table):
- 列出该类实现的所有接口。
字节码文件的这些组成部分共同定义了Java类的结构和行为。当JVM加载一个类时,它会解析字节码文件并根据其中的信息创建出相应的Class对象,然后再在JVM上执行该类的代码。字节码文件的结构设计使得Java程序具有很强的跨平台性,可以在任何安装了兼容JVM的设备上运行。
很多同学都不理解这个魔数,因此我们换种说法解释一下:由于我们可以随意的更改一个文件的后缀,例如把jpg更改为png。因此电脑是无法通过文件扩展名来确定文件类型的。因此如电脑需要通过文件的头几个字节(文件头)去校验文件的类型。而在JAVA字节码文件中,将文件头叫做魔数
3.简单阅读字节码
了解了字节码文件的组成部分之后,我们来看看一段字节码以及解释:
0 iconst_0 // 将int类型常量0压入操作数栈顶1 istore_1 // 将操作数栈顶的int类型数值(0)存入第二个局部变量槽中(局部变量索引1)2 iload_1 // 从局部变量表中加载索引为1的int类型值到操作数栈顶3 iinc 1 by 1 // 将局部变量表中索引为1的int类型变量增加16 istore_1 // 将操作数栈顶的int类型数值(经过iinc后的值)存入第二个局部变量槽中(局部变量索引1)7 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;> // 获取类java.lang.System的静态字段out的值,即PrintStream对象,并压入操作数栈顶
10 iload_1 // 从局部变量表中加载索引为1的int类型值到操作数栈顶
11 invokevirtual #3 <java/io/PrintStream.println : (I)V> // 调用PrintStream对象的println方法打印int值
14 return // 从当前方法返回
这段字节码对应的代码就是:
看不懂的同学们对着我的字节码注释自己尝试一步一步走一下就理解了。其实我们通过字节码就可以清晰的知道为什么代码运行结果等于0。
我们再来看一眼这个代码的:
他的字节码指令为:
0 iconst_01 istore_12 iinc 1 by 15 iload_16 istore_17 getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
10 iload_1
11 invokevirtual #3 <java/io/PrintStream.println : (I)V>
14 return
由此我们可以看出:之所以i++和++i不一样,从字节码角度上来看,是因为iinc和iload的执行顺序不一样,导致一个在打印的时候加载的是旧值,一个是新值。
总结来讲:int i=0; i = i ++之所以结果等于0,是因为在字节码中,i ++ 先把 0 取出来放入到临时的操作数栈中,接下来对 i 进行加 1 操作,i 变为了1,最后再把操作数栈中之前保存的i值拿出来放入i中,使得i变为了0。
而int i=0; i = ++ i 之所以结果等于 1 ,是因为在字节码中,++i 先进行自增,使得i变为 1 ,然后再将 1 取出来放入到临时的操作数栈中,再把操作数栈中的1拿出来赋值为i,使得i变为了 1 .
总结:
当我们编写和阅读Java代码时,我们通常关注的是高级语言层面的代码。然而,在Java编译器将高级代码转换为可执行的机器码之前,它会将我们的代码转换为一种称为字节码的中间表示形式。
字节码是一种与特定平台无关的指令集,它使用单字节的操作码和操作数来描述操作。它是Java虚拟机(JVM)的基本指令集,JVM可以解释执行字节码或将其编译为机器码。
通过深入理解字节码的组成,我们可以更好地理解Java代码的底层运行原理,进一步优化性能和调试问题。
如果我的内容对你有帮助,请点赞,评论,收藏。创作不易,大家的支持就是我坚持下去的动力!