在进入正文之前为了帮助大家更好的学习小编在此整理了很多的学习资料,欢迎大家在文末进行领取。
写一个简单的 Demo.java 程序如下所示
1 package com.lijiankun24.classpractice;
2
3 public class Demo {
4
5 private int m;
6
7 public int inc() {
8 return m + 1;
9 }
10 }
使用 javac 命令编译 Demo.java 文件生成 Demo.class 文件
1 $ javac Demo.java
接着用文本编辑器打开生成的 Demo.class 文件,如下所示
可以看到,该文件中是由十六进制符号组成的,这一段十六进制符号组成的长串是遵守Java 虚拟机规范的
在 Java 虚拟机规范中规定了 Java 虚拟机结构、Class 类文件结构、字节码指令等内容,可以参考 GitHub 上的《Java 虚拟机规范》
可以说 Java 虚拟机有两大特性:平台无关性和语言无关性,本篇文章主要介绍语言无关性的重要知识:.class 文件结构
Java 虚拟机就是一个虚拟的计算机,与真实的计算机一样,Java 虚拟机有自己完善的硬件体系,如处理器、堆栈、寄存器,还有相应的指令集系统。虚拟机与真实电脑的唯一区别就是:虚拟机的处理器、内存堆栈是用软件虚拟出来的,而真实的电脑的处理器、内存则是真真实实存在的
在 Java 虚拟机规范中,介绍的 Java 虚拟机的整体架构、Java 虚拟机内存区域、垃圾回收、.class 文件结构、类加载机制和 Java 虚拟机指令集。在本篇文章中主要介绍 .class 文件结构,其他内容可以查阅相关书籍和 Java 虚拟机规范
.class 文件是一组以 8 位字节为基础单位的二进制流,各数据项目严格按照顺序紧凑地排列在 .class 文件中,中间没有添加任何分隔符,这使得整个 .class 文件中存储的内容几乎全都是程序需要的数据,没有空隙存在
2.无符号数属于最基本的数据类型,以 u1、u2、u4、u8 分别代码 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值
表是一种复合数据结构,由无符号数或其他表构成,所有表都习惯性地以“info” 结尾
在 .class 中有一个集合的概念。集合表示同一类数据项的集合,一般是由一个前置的计数器加若干个连续的同样类型的数据项组成,计数器表示此集合中数据项的个 数,数据项是真正的数据内容
上面的表其实可以划分为以下七个部分,.class 字节码文件包括:
我们通过 Demo.class 为例讲解 .class 文件的 7 个部分
在魔数和 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 文件的常量池内容了,常量池中存放的数据一般分为两种类型:字面量和符号引用
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 常量的结构如下所示:
3. cp_info_constant_pool[2]:偏移地址是0x000F,内容是:0x0900030010,0x09 表示此常量是一个 CONSTANT_Fieldref_info 常量,0x0003 表示一个索引,指向常量池第 3 个常量所表示的信息;0x0010 是一个索引,表示指向常量池第 16 个常量所表示的信息。CONSTANT_Fieldref_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 的 UTF-8 编码的字符串的内容: I。
cp_info_constant_pool[7]:偏移地址是0x0022,内容是:
0x0100063C696E69743E。0x01 表示此常量是一个 CONSTANT_Utf8_info 常量,0x0006 表示字符串长度为 6,0x3C696E69743E 表示长度为 6 的 UTF-8 编码的字符串的内容: 。
上面分析了 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,表示父类的全限定名
字段表集合用于描述接口或类中声明的变量。这里说的字段包括类级变量(static 修饰)和对象级变量(没有用 static 修饰),但不包括方法中声明的局部变量。
字段表集合包括两部分:字段计数器和字段表,字段计数器表示有多少个字段,字段表的每个字段用一个名为 field_info 的表来表示,field_info 表的数据结构如下所示:
字段表都包含的固定数据项目到 descriptor_index 为止就结束了,不过在
descriptor_index 之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。在字段描述符之后,一般会有该字段的属性表集合,属性表集合有两部分,第一部分是属性计数器,第二部分是属性表。
在字段表集合中不会列出父类的字段,但是有可能会有一些 Java 代码中没有声明的字段, 比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段
在 Demo.class 文件中的字段表集合的偏移地址是:0x00B9,内容是:0x 0001 0002 0005 0006 0000.
0x0001 表示字段计数器是 1,表示只有 1 个字段
field_info_fields[0]:偏移地址是 0x00BB,内容是 0x0002 0005 0006 0000, 根据字段表的结构来分析这段数据
在字段表之后紧跟着方法表集合,方法表表示类或接口中的方法信息。
方法表集合和上述的字段表集合几乎完全一样,最开始的 2 个字节表示一个方法计数器, 在方法计数器之后,才是真正的方法数据项。方法表中的每个方法都用一个 method_info 表示,其数据结构如下:
在方法表结构中,我们可以看到方法的访问标志位、名称索引、描述符索引、属性表集合,方法中的代码在编译之后,放到方法属性表集合中的一个名为 “code” 的属性里面
在 Demo.class 中方法表集合的偏移地址是:0x00C3,方法表集合计数器是0x0002, 表示此方法表集合中有两个方法表数据项。可能有人会有疑问,Demo.java 中我们只写了一个方法,为什么在方法表中会有两个方法呢?因为编译器会自动添加实例构造器 方法
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。
那么我们就依次按照上表的结构分析此实例构造器的字节码内容,至于具体的字节码含义会
在后面的文章中分析介绍
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:00 00,end_pc:00 03
3.7.1 概念介绍 1. 在 class 文件、字段表、方法表都可以携带自己的属性表集合,用以描述某些场景专有的信息。 2. 属性表的格式是相对固定的,包括三部分内容:
一个 u2 的 attribute_name_index 指向常量池中的一个 UTF- 8 字符串常量表示一个属性名称
一个 u4 的数据类型表示 attribute_length 表示该属性值的字节长度
该长度的属性值信息,结构如下图所示:
对于属性表的限制来说相对较宽松,任何人实现的编译器都可以向属性表中写入自定义的属性值信息,Java 虚拟机对于它自己不认识的属性值则会忽略掉。
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
分析 .class 文件结构是比较枯燥无聊的,但是如果可以看懂 .class 文件结构的内容,并且理解其中的含义,知道 .class 文件结构中 Code 属性中字节码指令的执行过程,对我们的
Java 能力提升还是比较大的。
分析 .class 文件结构,我们可以使用 “javap -verbose Demo.class” 指令查看,我们也可以使用 010 Editor 软件分析,可以方便的查看各个数据项的地址偏移量、数据项内容。比如,我们想查看第 4 个常量池的内容,如下图所示
写在最后,这篇文章分析了 .class 文件的结构,知道了其本质是以 8 位字节为单位存储的二进制流文件
本文分享到这里就结束了,了解更多Java知识请关注微信公众号“老周扯IT”