一. 简介
写一个简单的 Demo.java 程序如下所示
使用 javac 命令编译 Demo.java 文件生成 Demo.class 文件
接着用文本编辑器打开生成的 Demo.class 文件,如下所示
可以看到,该文件中是由十六进制符号组成的,这一段十六进制符号组成的长串是遵守Java 虚拟机规范的
二. Java 虚拟机规范
在 Java 虚拟机规范中规定了 Java 虚拟机结构、Class类文件结构、字节码指令等内容,可以参考 GitHub 上的《Java虚拟机规范》
Java 虚拟机
-
可以说 Java虚拟机有两大特性:平台无关性和语言无关性,本篇文章主要介绍语言无关性的重要知识:.class 文件结构
-
Java 虚拟机就是一个虚拟的计算机,与真实的计算机一样,Java虚拟机有自己完善的硬件体系,如处理器、堆栈、寄存器,还有相应的指令集系统。虚拟机与真实电脑的唯一区别就是:虚拟机的处理器、内存堆栈是用软件虚拟出来的,而真实的电脑的处理器、内存则是真真实实存在的
-
在 Java 虚拟机规范中,介绍的 Java 虚拟机的整体架构、Java虚拟机内存区域、垃圾回收、.class 文件结构、类加载机制和 Java虚拟机指令集。在本篇文章中主要介绍 .class文件结构,其他内容可以查阅相关书籍和 Java 虚拟机规范
class 类文件结构
.class 文件是一组以 8位字节为基础单位的二进制流,各数据项目严格按照顺序紧凑地排列在 .class 文件中,中间没有添加任何分隔符,这使得整个 .class文件中存储的内容几乎全都是程序需要的数据,没有空隙存在
-
.class 文件是以类似于 C语言结构体的结构来存储数据的,其中存储的数据有两种:无符号数和表
-
无符号数属于最基本的数据类型,以 u1、u2、u4、u8 分别代码 1 个字节、2个字节、4 个字节和 8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8 编码构成的字符串值
-
表是一种复合数据结构,由无符号数或其他表构成,所有表都习惯性地以”info”结尾
-
在 .class中有一个集合的概念。集合表示同一类数据项的集合,一般是由一个前置的计数器加若干个连续的同样类型的数据项组成,计数器表示此集合中数据项的个 数,数据项是真正的数据内容
-
整个 .class文件本质上就是一张表,由下表所示的数据项构成
1. 上面的表其实可以划分为以下七个部分,.class 字节码文件包括:
魔数与class文件版本 常量池 访问标志 类索引、父类索引、接口索引 字段表集合 方法表集合 属性表集合
三. class 文件详解
我们通过 Demo.class 为例讲解 .class 文件的 7 个部分
魔数和 class 文件版本
- 概念介绍
在魔数和 class 文件版本中有如下四点需要介绍:
-
魔数(Magic Number):.class 文件的第 1 - 4个字节,它唯一的作用就是确定这个文件是否是一个能被虚拟机接受的 class文件,其固定值是:
0xCAFEBABE
(咖啡宝贝)。如果一个 class文件的魔术不是 0xCAFEBABE,那么虚拟机将拒绝运行这个文件 -
次版本号(minor version):.class 文件的第 5 - 6个字节,即编译生成该 .class 文件的 JDK 次版本号
-
主版本号(major version):.class 文件的第 7 -8个字节,即编译生成该 .class 文件的 JDK 主版本号
-
Note:高版本的 JDK 能向下兼容低版本的 .class文件,但不能运行新版本的.class 文件。例如一个 .class 文件是使用 JDK 1.5 编译的,那么我们可以用JDK 1.7 虚拟机运行它,但不能用 JDK 1.4 虚拟机运行它。各个版本的 SDK的次版本号和主版本号如下表所示
- 示例
在上面的 Demo.class 文件中,Magic Number:0xcafe babe
,minor version:0x0000
,major version:0x0034
,可见我们是使用 JDK 1.8 编译生成的Demo.class 文件
常量池
- 概念介绍
紧接着版本号之后的是常量池的入口,常量池可以理解为 class文件之中的资源仓库,它是占用 class 文件空间最大的数据项之一。
常量池是一个集合,它由两部分组成:常量池计数器和常量池
-
常量池计数器(constant_pool_count) 是一个 u2 的无符号数
-
常量池(constant_pool):紧跟在常量池计数器后面的内容就是该 .class文件的常量池内容了,常量池中存放的数据一般分为两种类型: 字面量和符号引用: 字面量:是指文本字符串、声明为 final的常量值等 符号引用:是一个更偏向于编译原理方面的概念,主要包括三类常量: 1).类和接口的全限定名, 2).字段的名称和描述符, 3). 方法的名称和描述符
-
在常量池中的常量共有 14种类型,每个常量都是一个表,每一个表都有各自的组成结构。这 14个常量有一个公共的特点,就是每个常量开始是一个用 u1类型的无符号数表示的标志位(tag,取值见下表),表示此常量属于哪种常量类型
- 示例
在上面的 Demo.class 文件中,常量池开始的偏移地址是:0x0008
。
-
首先是常量计数器(constant_pool_coun),数值是:
0x0013
,表示此Demo.class文件中共有 18 个常量 -
cp_info_constant_pool[1]:偏移地址是
0x000A
,内容是:0x0A0004000F
。0x0A 标志位表示是一个 CONSTANT_Methodref_info 常量,0x0004
是一个索引,指向常量池中第 4个常量所表示的信息;0x000F 是一个索引,指向常量池第15个常量所表示的信息。CONSTANT_Methodref_info 常量的结构如下所示:
-
cp_info_constant_pool[2]:偏移地址是
0x000F
,内容是:0x0900030010,0x09表示此常量是一个 CONSTANT_Fieldref_info 常量,0x0003
表示一个索引,指向常量池第 3 个常量所表示的信息;0x0010 是一个索引,表示指向常量池第 16个常量所表示的信息。CONSTANT_Fieldref_info 常量的结构如下所示: -
cp_info_constant_pool[3]:偏移地址是
0x0014
,内容是:0x070011
。0x07
标志位表示此常量 是一个 CONSTANT_Class_info 常量,索引0x0011
指向常量池中第 17 个常量。CONSTANT_Class_info 常量的结构如下所示:
-
cp_info_constant_pool[4]:偏移地址是0x0017,内容是:0x070012。0x07标志位表示此常量是一个 CONSTANT_Class_info 常量,索引 0x0012指向常量池中第 18 个常量。
-
cp_info_constant_pool[5]:偏移地址是0x001A,内容是:0x0100016D。0x01表示此常量是一个 CONSTANT_Utf8_info 常量,0x0001 表示 UTF-8编码的字符串占用的字节数;0x6D 表示 长度为 1 的 UTF-8 编码的字符串的内容:m。
CONSTANT_Utf8_info 常量的结构如下所示:
-
cp_info_constant_pool[6]:偏移地址是0x001E,内容是:0x01000149。0x01表示此常量是一个 CONSTANT_Utf8_info 常量,0x0001 表示 UTF-8编码的字符串占用的字节数;0x49 表示长度为 1 的 UTF8编码的字符串的内容: I。
-
cp_info_constant_pool[7]:偏移地址是0x0022,内容是: 0x0100063C696E69743E。0x01 表示此常量是一个 CONSTANT_Utf8_info常量, 0x0006 表示字符串长度为 6,0x3C696E69743E 表示长度为 6 的 UTF-8 编码的字符串的内容: <init>。
上面分析了 7 个常量,其余的常量也是类似的方法。根据第一个 u1的标志位,就知道这个常量的类型和表结构,就可以知道这个常量的长度大小和代表的含义了。我们也可以通过”javap-verbose
” 命令查看 .class 文件的内容,如下图所示:
访问标志
- 概念介绍
常量池之后是 u2类型的访问标志位(access_flags),这个访问标志位用于标识类或者接口层次的访问信息,包括:这个Class是类还是接口、是否定义为public类型、是否定义为abstract类型,如果是类的话,是否被final 关键字修饰。具体的标志位以及标志的含义见下表
- 示例
在 Demo.class 文件中访问标志位是:0x0021
。在上表中,我们并没有发现 00 21的访问标志,这是因为在字节码文件中的访问标志,可以通过上表中多个访问标志通过或运算组成真正的访问标志。通过上表中的ACC_SUPER 和 ACC_PUBLIC 就可以组合中 00 21的访问标志了,也就是说该类的访问标志是 public 且允许使invokespecial字节码指令的新语义的
类索引、父类索引、接口索引
- 概念介绍
在 .class 文件中由这三项数据来确定这个类的继承关系。
-
类索引:u2 数据类型,用于确定这个类的全限定名。
-
父类索引:u2 数据类型,用于确定这个类的父类的全限定名。
-
接口索引:u2数据类型的集合,用于描述类实现了哪些接口,这些被实现的接口将按照 implements语句后的顺序从左至右排列在接口索引集合中。接口索引集合分为两部分,第一部分表示接口计数器(interfaces_count),是一个u2 类型的数据,第二部分是接口索引表表示接口信息,紧跟在接口计数器之后。若一个类实现的接口为0,则接口计数器的值为 0,接口索引表不占用任何字节。
-
示例
在此 Demo.class 文件中,类索引、父类索引、接口索引分别如下:
-
类索引:偏移地址是 0x00B3,内容是 0x0003,表示其指向了常量池中第 3个常量CONSTANT_Class_info,第 3 个常量索引指向第 17 个常量,第 17个常量是一个 UTF-8 编码的字符串,其值是:com/lijiankun24/classpractice/Demo,表示此类的全限定名
-
父类索引:偏移地址是 0x00B5,内容是 0x0004,其指向了常量池中第 4个常量CONSTANT_Class_info,第 4 个常量索引指向第 18 个常量,第 18个常量的值是:java/lang/Object,表示父类的全限定名
-
接口索引:偏移地址是 0x00B7,内容是 0x0000。因为 Demo类没有实现任何接口,所以接口索引的计数器是 0,表示没有接口索引。
字段表集合
- 概念介绍
字段表集合用于描述接口或类中声明的变量。这里说的字段包括类级变量(static修饰)和对象级变量(没有用 static 修饰),但不包括方法中声明的局部变量。
字段表集合包括两部分:字段计数器和字段表,字段计数器表示有多少个字段,字段表的每个字段用一个名为 field_info 的表来表示,field_info 表的数据结构如下所示:
字段表都包含的固定数据项目到 descriptor_index 为止就结束了,不过在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。在字段描述符之后,一般会有该字段的属性表集合,属性表集合有两部分,第一部分是属性计数器,第二部分是属性表。
在字段表集合中不会列出父类的字段,但是有可能会有一些 Java代码中没有声明的字段,比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段
- 示例
在 Demo.class 文件中的字段表集合的偏移地址是:0x00B9
,内容是:0x 000100020005 0006 0000
.
- 0x0001 表示字段计数器是 1,表示只有 1 个字段
2. field_info_fields[0]:偏移地址是 0x00BB,内容是 0x0002 0005 0006 0000, 根据字段表的结构来分析这段数据0x0002 表示该字段的访问标识,0002表示是 private 的
0x0005表示该字段的名称索引项,指向常量池中的第 5 个常量,第 5 个常量是一个UTF-8 的字符串 m
0x0006表示该字段的描述符索引项,指向常量池中的第 6 个常量,第 6 个常量是一个UTF-8 的字符串 I,I 描述符表示是一个int 类型的字段
0x0000 表示 m字段的属性表集合,属性表集合计数器是 0,表示此字段没有额外的属性信息。
方法表集合
- 概念介绍
在字段表之后紧跟着方法表集合,方法表表示类或接口中的方法信息。
方法表集合和上述的字段表集合几乎完全一样,最开始的 2 个字节表示一个方法计数器, 在方法计数器之后,才是真正的方法数据项。方法表中的每个方法都用一个 method_info 表示,其数据结构如下:
在方法表结构中,我们可以看到方法的访问标志位、名称索引、描述符索引、属性表集合,方法中的代码在编译之后,放到方法属性表集合中的一个名为”code” 的属性里面
-
示例
-
在 Demo.class中方法表集合的偏移地址是:0x00C3,方法表集合计数器是0x0002, 表示此方法表集合中有两个方法表数据项。可能有人会有疑问, Demo.java中我们只写了一个方法,为什么在方法表中会有两个方法呢?因为编译器会自动添加实例构造器 <init> 方法
-
method_info_methods[0]:偏移地址是:0x00C5,内容是:0x00 0100 0700 0800 0100 0900 0000 1d00 0100 0100 0000 052a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003。
-
0x0001:access_flags表示ACC_PUBLIC,即表示该方法是public 的 0x0007:name_index表示方法名称索引,指向常量池中的第 7 个常量,第 7 个常量是一个 UTF-8字符串,值是:<init> 0x0008:descriptor_index表方法描述符索引项,指向常量池中的第 8 个常量,是一个 UTF-8字符串,值是:()V 0x0001:表示此方法的属性表集合计数器,有1 个属性0x0009:表示此属性的 attribute_name_index,指向常量池中的第 9 UTF-8 常量:Code,说明此属性是方法的字节码描述Code 属性
那么我们就依次按照上表的结构分析此实例构造器的字节码内容,至于具体的字节码含义会
在后面的文章中分析介绍
0x0009:上面已经介绍过,表示此 Code 属性的名称索引,其值就是”Code”字符串
0x0000 001d:attribute_length 表示属性长度为 29个字节0x0001:max_stack 表示操作数栈的最大深度是 1
0x0001:max_locals 表示局部变量表的最大长度是 1
0x0000 0005:code_length 表示字节码指令长度是 5,共有 5 个字节码指令
0x2ab7 0001 b1:这 5 个 u1 数据,表示 3 个字节码指令,0x2a = aload_0, 0xb7 = invokespecial,0x0001 = 表示一个指向常量池的索引,是 invokespecial 指令的参数,0xb1 = return 表示从当前方法返回
0x0000 exception_table_length=0,异常表集合长度为 0
0x0001:attributes_count=1(Code属性表内部还含有1个属性表) 0x000a:指向常量池中的第十个常量:LineNumberTable,
LineNumberTable 属性结构如下图所示,内容是:0000 0006 0001 0000 0003
0x0000 0006:attribute_length 表示属性长度为 6 0x0001:line_number_table_length,表示后面的 line_number_info 表有 1 个,line_number_info表包括了 start_pc 和 line_number 两个 u2类型的数据项, 前者是字节码行号,后者是 java 源码行号:start_pc:0000,end_pc:00 03
- method_info_methods[1]:上面我们分析了第一个方法:实例构造器方法,分析流程就是上面这样,方法表有固定的结构,其中包含一些固定的信息,包括操作数栈最大深度、局部变量表最大长度、以及很重要的Code 属性,在 Code 属性中包含 java方法编译生成的字节码指令,如果想快速的浏览方法表集合的内容,也可以使用”
javap -verbose Demo.class
” 指令查看,如下图所示
属性表集合
-
概念介绍
-
在 class文件、字段表、方法表都可以携带自己的属性表集合,用以描述某些场景专有的信息。
-
属性表的格式是相对固定的,包括三部分内容: 一个 u2 的 attribute_name_index指向常量池中的一个 UTF- 8 字符串常量表示一个属性名称 一个 u4 的数据类型表示attribute_length 表示该属性值的字节长度 该长度的属性值信息,结构如下图所示:
-
对于属性表的限制来说相对较宽松,任何人实现的编译器都可以向属性表中写入自定义的属性值信息,Java 虚拟机对于它自己不认识的属性值则会忽略掉。
1. 在 Java 7 虚拟机规范中已经预定义了 21 项属性
-
示例
-
Demo.class 中属性表的偏移地址是:0x011D,内容是 0x00 0100 0D00 0000 0200 0E
-
0x0001 表示此属性表集合的计数器是1,有 1 个属性
-
attribute_info_attributes[0]:偏移地址是:0x011F,内容是 0x00 0D00 0000 0200 0E
-
0x000D:指向常量池中的第 13 个 Utf-8常量:SourceFile, SourceFile 属性用于记录生成这个 Class 文件的源码文件名称,其结构如下图所示:
0x0000 0002:attribute_length 属性长度是 2 00 0E:sourcefile_index 指向常量池中第14 个常量Demo.java
010 Editor
分析 .class 文件结构是比较枯燥无聊的,但是如果可以看懂 .class文件结构的内容,并且理解其中的含义,知道 .class 文件结构中 Code属性中字节码指令的执行过程,对我们的Java 能力提升还是比较大的。
分析 .class 文件结构,我们可以使用 “javap -verbose Demo.class
”指令查看,我们也可以使用 010 Editor
软件分析,可以方便的查看各个数据项的地址偏移量、数据项内容。比
如,我们想查看第 4 个常量池的内容,如下图所示
写在最后,这篇文章分析了 .class 文件的结构,知道了其本质是以 8位字节为单位存储的二进制流文件
1个字节=2个16进制字符