深入理解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文件的结构对于进一步理解虚拟机执行引擎非常重要,所以这是基础且重要的一步。