深入理解Java字节码文件:从编译到执行的完整过程解析

也许你写了无数行代码,也许你可以非常流畅地使用高级语言,但你可能不了解那些高级语言的执行过程。比如现在流行的Java。

深入理解Java字节码文件:从编译到执行的完整过程解析

Java号称是“一次编译,到处运行”的语言,但是我们对这句话理解有多深呢?从我们写入的java文件到编译器编译出的java字节码文件(即.class文件),这个过程就是java编译过程;我们的java虚拟机执行字节码文件。无论字节码文件来自哪里,由哪个编译器编译,甚至是手写的字节码文件,只要符合Java虚拟机的规范,就可以执行该字节码文件。所以这篇文章主要讲的是java字节码文件相关的知识。接下来我们通过一个具体的Demo来深入了解:

1 首先我们来写一个java源文件

上面是我们写的一个java程序。这很简单。它只有一个成员变量a和一个方法testMethod()。

2 接下来我们使用javac命令或者ide工具将java源文件编译成java字节码文件。

上图是编译好的字节码文件。我们可以看到一堆十六进制字节。如果你用IDE打开的话,你可能会看到反编译出来的熟悉的java代码,而这就是纯字节码,这就是我们今天要讲的重点。

也许你会对这么一堆字节码感到头疼,不过没关系。我们会慢慢地为你尝试去理解,你可能会得到不同的结果。开始之前我们先看一张图

这张图是java字节码的概述。我们按照上面的顺序解释字节码。它总共包含10个部分,包括幻数、版本号、常量池等,接下来我们按照顺序一步步解读。

3.1 幻数

从上面的概览图我们知道前4个字节代表幻数,我们的Demo对应的是0XCAFE BABE。什么是神奇数字?幻数是用于区分文件类型的符号。一般用文件的前几个字节来表示。比如0XCAFE BABE代表的是一个class文件,那么有人可能会问,能否通过文件名后缀来判断文件类型呢?可以,但是文件名是可以修改的(包括后缀),所以为了保证文件的安全,在文件内部写入了文件类型,保证不被篡改。

从Java的字节码文件类型中我们看到CAFE BABE翻译过来就是咖啡宝贝,然后再看Java的图标。

CAFE BABE=咖啡。

3.2 版本号

确定文件类型后,下一步就是了解版本号。版本号包括主版本号和次版本号,各占2个字节。在这个Demo中,类型是0X0000 0033。前0000是次版本号,后面的0033是主版本号。通过十六进制转换,次版本号为0,主版本号为51。

从Oracle官网我们可以知道51对应的是官方的jdk1.7,第二个版本是0,所以这个文件的版本是1.7.0。如果需要验证,可以使用java –version命令输出版本号,或者修改编译目标version –target并重新编译,检查编译后的字节码文件的版本号是否有相应修改。

至此,我们已经了解了前8个字节的含义。下面说一下常量池相关的内容。

3.3 常量池

紧接着主版本号的是常量池条目。常量池是Class文件中的资源仓库。在后面的内容中,我们会发现很多地方都会涉及到,比如Class Name、Interfaces等。常量池中存储的常量主要有两种类型:文字和符号引用。文字如文本字符串、Java中声明为final的常量值等,而符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。

为什么我们需要类和接口的全局限定名称?系统引用一个类或者接口的时候,不都是通过内存地址来操作的吗?这里仔细想想,Java虚拟机在没有将类加载到内存中的时候,根本就没有分配内存地址,所以也就没有对内存进行任何操作。因此,Java虚拟机首先需要将类加载到虚拟机中,然后这个流程设计对该类进行定位(需要加载包A下的类B,不能加载到其他包下的其他类中),所以唯一性需要通过全局限定名称来确定。这就是为什么叫全球性,意思是限制性,也就是唯一性。

在进行具体的常量池分析之前,我们先了解一下常量池的项目类型表:

上表描述了11种数据类型的结构。事实上,jdk1.7之后又添加了3种类型(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。这样加起来总共有14种。接下来我们根据Demo的字节码一一翻译。

0×0015:由于常量池的数量不固定(n+2),因此需要在常量池的入口处放置一个u2类型的数据来表示常量池的数量。因此,十六进制数为21,即有20个常量,索引范围为1~20。明明是21,为什么是20呢?由于Class文件格式规定,设计者保留了第0项以备后患。从这里我们知道接下来需要翻译这 20 个常数。

常量 #1(总共有 20 个常量,这是第一个,依此类推...)

0x0a-:从常量类型表中,我们发现第一个数据是u1类型的标签。十六进制的0a,十进制的10,对应表中的MethodRef_info。

0x-00 04-:Class_info索引项#4

0x-00 11-:NameAndType 索引条目#17

常数#2

0x-09:FieldRef_info

0×0003:Class_info索引项#3

0×0012:NameAndType索引条目#18

常数#3

0×07-:类信息

0x-00 13-:全局限定名常量索引为#19

常数#4

0x-07:类信息

0×0014:全局限定名常量索引为#20

常数 #5

0×01:UTF-8_信息

0x-00 01-:字符串长度为1(选择下一个字节长度进行转义)

0x-61:“a”(十六进制到 ASCII 字符)

常数#6

0×01:UTF-8_信息

0x-00 01:字符串长度为1

0x-49:“我”

常数#7

0×01:UTF-8_信息

0x-00 06:字符串长度为6

0x-3c 696e 6974 3e-:””

常数#8

0×01:UTF-8_信息

0×0003:字符串长度为3

0×2829 56:“()V”

常数 #9

0x-01:UTF-8_信息

0×0004:字符串长度为4

0x436f 6465:“代码”

常数#10

0×01:UTF-8_信息

0×00 0f:字符串长度为15

0x4c 696e 654e 756d 6265 7254 6162 6c65:"行号表"

常数#11

ox01:UTF-8_信息

0×00 12 字符串长度为18

0x-4c 6f63 616c 5661 7269 6162 6c65 5461 626c 65:“本地变量表”

常数 #12

0×01:UTF-8_信息

0×0004 字符串长度为4

0×7468 6973:“这个”

常数 #13

0×01:UTF-8_信息

0x0f:字符串长度为15

0x4c 636f 6d2f 6465 6d6f 2f44 656d 6f3b:“Lcom/演示/演示;”

常数 #14

0×01:UTF-8_信息

0×00 0a:字符串长度为10

ox74 6573 744d 6574 686f 64:“测试方法”

常数 #15

0×01:UTF-8_信息

0x000a:字符串长度为10

0x536f 7572 6365 4669 6c65:“源文件”

常数#16

0×01:UTF-8_信息

0×0009:字符串长度为9

0x-44 656d 6f2e 6a61 7661:“演示.java”

常数 #17

0x0c :名称和类型_信息

0×0007:字段或名称常量项索引#7

0×0008:字段或方法描述符常量索引#8

常数 #18

0x0c:名称和类型_信息

0×0005:字段或名称常量项索引#5

0×0006:字段或方法描述符常量索引#6

常数 #19

0×01:UTF-8_信息

0×00 0d:字符串长度为13

0×63 6f6d 2f64 656d 6f2f 4465 6d6f:“com/demo/Demo”

常数#20

0×01:UTF-8_信息

0×00 10:字符串长度为16

0x6a 6176 612f 6c61 6e67 2f4f 626a 6563 74:“java/lang/对象”

到目前为止我们已经解析了所有常量。下一步是解析访问标志位。

3.4 Access_Flag 访问标志

访问标志信息包括Class文件是类还是接口、是否定义为public、是否为abstract、如果是类则是否声明为final。从上面的源代码我们知道该文件是一个类,并且是public的。

0x 00 21:0×0020和0×0001的并集。其中,标志值0×0020涉及字节码指令。后面会有专题来讲解字节码指令。期待它...

3.5 类别索引

类索引用于确定类的完全限定名称

0×00 03表示引用第三个常量,同时第三个常量引用第19个常量,搜索结果为“com/demo/Demo”。 #3.#19

3.6 父类索引

0×00 04 同理:#4.#20(java/lang/Object)

3.7 接口索引

从java_byte.jpeg图中我们知道这个接口有2+n个字节。前两个字节表示接口数量,后面是接口表。我们的类没有任何接口,所以应该是0000。果然,搜索字节码文件时,结果是0000。

3.8 字段表集合

字段表用于描述类和接口中声明的变量。这里的字段包括类级变量和实例变量,但不包括方法内声明的局部变量。

同样,接下来是2+n个字段属性。我们只有一个属性a,应该是0001。搜索文件结果是0001。

接下来我们必须解析这些字段。附字段表结构图

0×00 02:访问标志是私有的(自搜索字段访问标志)

0×00 05:字段名索引为#5,对应“a”

0x 00 06:描述符索引为#6,对应“I”

0x 00 00:属性表数量为0,因此没有属性表。

Tips:一些不太重要的表(字段、方法访问标志表)可以自行查找,这里就不贴出来了,避免篇幅过大。

3.9 方法

我们只有一个方法testMethod,逻辑上前2个字节应该是0001,搜索发现是0×00 02,这是什么原因呢?这是否意味着有两种方法?并继续阅读...

上图是方法表结构图。根据这张图,我们分析如下字节码:

方法一:

0×00 01:访问标志ACC_PUBLIC,表示该方法是公共的。 (可以自行搜索flag表)

0×00 07:方法名索引为#7,对应“”

0×00 08:方法描述符索引为#8,对应“()V”

0×00 01:属性表数量为1(一个属性表)

那么属性表就来了。什么是属性表?可以理解为是描述一些专有信息。上面的方法有一个属性表。所有属性表的结构如下:

u2的属性名称索引,u2的属性长度加上属性长度的信息。

虚拟机规范中有很多预定义的属性,比如Code、LineNumberTable、LocalVariableTable、SourceFile等,可以在网上搜索到。

根据上面的表结构分析,得到以下信息:

0×0009:名称索引为#9(“代码”)。

0×000 00038:属性长度为56字节。

然后解析一个Code属性表,按照下图进行分析

前6个字节(名称索引2个字节+属性长度4个字节)已经解析完毕,那么下一步就是解析剩下的56-6=50个字节。

0×00 02:最大堆栈=2

0×00 01:max_locals=1

0×00 0000 0a:code_length=10

0x2a b700 012a 04b5 0002 b1:这是代码代码,可以通过虚拟机字节码指令找到。

2a=aload_0(将第一个引用变量压入栈顶)

b7=invokespecial(调用父类构造函数)

00=什么都不做

01 = 将 null 推入栈顶

2a=同上

04=iconst_1 将 int 类型 1 压入栈顶

b5=putfield 给指定类的实例变量赋值

00=同上

02=iconst_m1 将 int 类型 -1 压入栈顶

b1=return 从当前方法返回 void

整理并删除无操作指令以获得以下内容

0:aload_0

1:调用特殊

4:aload_0

5:图标_1

6:普特菲尔德

9:返回

关于虚拟机字节码指令的内容,我们稍后会继续深入研究……现在,你只需要了解它即可。接下来继续沿着Code属性表解析:

0×00 00:异常表长度=0

0×00 02:attributes_count=2(代码属性表还包含2个属性表)

0×00 0a:第一个属性表是“LineNumberTable”

0×00 0000 0a : “属性长度为10”

0×00 02:行号表长度=2

line_number_table 是 line_number_table_length 和类型 line_number_info 的集合。 line_number_info表包括两个u2类型的数据项:start_pc和line_number。前者是字节码行号,后者是Java源代码行号。

0×00 00:start_pc =0

0×00 03:end_pc =3

0×00 04:start_pc=4

0×00 04 : end_pc=4

0×00 0b 第二个属性表为:“LocalVariableTable”

0×00 0000 0c:属性长度为12

0×00 01:本地变量表长度=1

然后根据local_variable_info表结构解析:

0×00 00:start_pc=0

0×00 0a:长度=10

0x000c:name_index=”这个”

0x000d:描述符索引#13(“Lcom/demo/Demo”)

0000 索引=0

//——-第一个方法的分析到这里就完成了——-//

Method()–1属性代码表-2属性表(LineNumberTable,LocalVariableTable)接下来解析第二个方法

第二种方法:

0×00 04:“受保护”

0×00 0e:#14(“测试方法”)

0×00 08:“()V”

0×0001:属性个数=1

0×0009:“代码”

0×0000 002b 属性长度为43

解析代码表

0000:最大堆栈=0

0001:最大本地=1

0000 0001:代码长度=1

0xb1:返回(该方法返回void)

0×0000 异常表长度=0

0×0002 属性表长度为2

//第一个属性表

0x000a:#10,行号表

0×0000 0006:属性长度为6

0×0001:行号长度=1

0×0000:start_pc =0

0×0008:end_pc =8

//第二个属性表

0x000b:#11,本地变量表

0×0000 000c:属性长度为12

0×0001: local_variable_table_length =1

0×0000 :start_pc = 0

0×0001:长度=1

0x000c:name_index =#12“这个”

0x000d:描述索引#13“Lcom/demo/Demo;”

0000 索引=0

//至此,方法解析完成。回顾一下最上面的解析序列图,接下来我们将解析Attributes。

3.10 属性

0×0001:同样,表示有1个属性。

0x000f: #15("源文件")

0×0000 0002 属性长度=2

0×0010: sourcefile_index = #16(“Demo.java”)

SourceFile属性用于记录生成Class文件的源文件的名称。

4 又一个字

其实写这么多对于我们来说确实很麻烦,但是再次经历这个过程我们收获的还是不一样的。现在,使用Java自带的反编译器来解析字节码文件。

javap -verbose Demo //无需添加后缀.class

5 总结

至此讲解就完成了class文件的分析,方便我们以后也能理解字节码文件。理解class文件的结构对于进一步理解虚拟机执行引擎非常重要,所以这是基础且重要的一步。

你可能想看:
« 上一篇
下一篇 »

相关推荐

java中什么是冒泡,冒泡算法

2026年09月06日

27阅读

java中冒号,java双冒号

2026年09月06日

26阅读

java中的->,java中的是什么意思

2026年09月06日

24阅读

java中的super用法,Java中super的说明

2026年09月06日

28阅读

java中反射的应用,java中反射的应用场景

2026年09月06日

25阅读

java中开关的定义,Java关键字有哪些

2026年09月06日

25阅读