字节码层面分析class类文件结构
一个面试题:Java 中 String 字符串的长度有限制么?
【答案】String 的长度是有限制的。
- 编译器的限制:字符串的 UTF-8 编码值的字节数不能超过 65535,字符串的长度不能超过 65534;
- 运行时的限制:字符串的长度不能超过 2^31-1,占用的内存数不能超过虚拟机能够提供的最大值。
- 长度为 2^31-1 的字符串所占用的空间大小为:4G。
Java 提供了一种在所有平台上都能使用的一种中间代码--字节码类文件(*.class文件)
- 有了字节码,无论哪种平台只要安装了虚拟机都可以直接运行字节码
- 有了字节码,解除了 Java 虚拟机和 Java 语言之间的耦合
一、Class中的数据结构
从纵观角度看,class 文件里只有两种数据结构:无符号数
和 表
。
【无符号数】
- 属于基本的数据类型。
- 以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4 个字节和 8 个字节的无符号数。
- 无符号数可以用来描述数字、索引引用、数量值或字符串(UTF-8编码)。
【表】
- 表是有多个无符号数或其他表作为数据项构成的复合数据类型。
- class 文件中所有的表都以 “_info” 结尾。
- 整个 Class 文件本质上就是一张表。
表和无符号之间的关系表和无符号之间的关系图
可用下面的伪代码表示
// 无符号数
byte[] u1 = new byte[1];
byte[] u2 = new byte[2];
byte[] u4 = new byte[4];
byte[] u8 = new byte[8];
// 表
class _table {
// 表中可以引用无符号数
u1 tag;
u4 index;
// 表中可以引用其他表
method_info table;
}
二、Class文件结构
无符号数和表组成了 class 中的各个结构。
这些结构按照 预先规定好的顺序
紧密的从前向后排列,相邻的项之间没有任何间隙。
class 文件结构如下
魔数 | 版本号 | 常量池 | 访问标识 | 类/父类/接口 | 字段描述集合 | 字段描述集合 | 属性描述集合 |
---|
当 JVM 加载某个 class 文件时,JVM 就是根据上图的结构进行解析 class 文件到内存中,并在内存中分配相应的空间。
每种结构所占用的空间大小如下表:
字段 | 名称 | 数据类型 | 数量 |
---|---|---|---|
magic number | 魔数 | u4 | 1 |
major version | 主版本号 | u2 | 1 |
minor version | 副版本号 | u2 | 1 |
constant_pool_count | 常量池大小 | u2 | 1 |
constant_pool | 常量池 | cp_info | countant_pool_count - 1 |
access_flag | 访问标识 | u2 | 1 |
this_class | 当前类索引 | u2 | 1 |
super_class | 父类索引 | u2 | 1 |
interfaces_count | 接口索引集合大小 | u2 | 1 |
interfaces | 接口索引集合 | u2 | interfaces_count |
fields_count | 字段索引集合大小 | u2 | 1 |
fields | 字段索引集合 | field_info | fields_count |
methods_count | 方法索引集合大小 | u2 | 1 |
methods | 方法索引集合 | method_info | methods_count |
attributes_count | 属性索引集合大小 | u2 | 1 |
attributes | 属性索引集合 | attribute_info | attributes_count |
示例
public class ClassHexNormal implements Serializable, Cloneable {
private int num = 1;
public int add(int i) {
int j = 10;
num = num + i;
return num;
}
}
将上述代码编译成 .class 文件,使用 16 进制编辑器打开:
16进制字节码文件下面我们通过上图来一步步解析字节码文件:
1、魔数 magic numebr
魔数在 class 文件开头的四个字节是 class 文件的魔数,它是一个固定值 0XCAFEBABE
。
魔数是 class 文件的标志,它是判断一个文件是不是 class 格式文件的标准。
2、版本号
版本号前两个字节 0000 代表 次版本号 minor_version
。后两个字节 0034 是 主版本号 major_version
,对应的十进制值为 52。
所以当前 class 文件的主版本号为 52,次版本号为 0,所以综合版本号是 52.0,也就是 jdk1.8.0。
3、常量池(重点)
紧跟在版本号之后的是一个叫做 常量池的表 cp_info
,在常量池中保存了类的各种相关信息。比如类的名称、父类的名称、类中的方法名、参数名称、参数类型等。
常量池中的每一项都是一个表,其项目类型共有14种:
表名 | 标识位 | 描述 |
---|---|---|
CONSTANT_uft8_info | 1 | UTF-8编码字符串表 |
CONSTANT_Integer_info | 3 | 整型常量表 |
CONSTANT_Float_info | 4 | 浮点常量表 |
CONSTANT_Long_info | 5 | 长整型常量表 |
CONSTANT_Double_info | 6 | 双精度浮点型常量表 |
CONSTANT_Class_info | 7 | 类、接口引用表 |
CONSTANT_String_info | 8 | 字符串常量表 |
CONSTANT_Fieldref_info | 9 | 字段引用表 |
CONSTANT_Methodref_info | 10 | 类的方法引用表 |
CONSTANT_InterfaceMethodref_info | 11 | 接口的方法引用表 |
CONSTANT_NameAndType_info | 12 | 字段或方法的名称和类型表 |
CONSTANT_MethodHandle_info | 15 | 方法句柄表 |
CONSTANT_MethodType_info | 16 | 方法类型表 |
CONSTANT_InvokeDynamic_info | 18 | 动态方法调用表 |
以 CONSTANT_Class_info 表为例:
table CONSTANT_Class_info {
u1 tag = 7;
u2 name_index;
}
【tag】:占用一个字节大小,比如值为 7,说明是 CONSTANT_Class_info
类型表。
【name_index】:是一个索引值,可以理解为一个指针指向常量池汇总索引为 name_index
的常量表,比如 name_index = 2
,则它指向常量池中第 2 个常量。
以 CONSTANT_Uft8_info 表为例:
table CONSTANT_uft8_info {
u1 tag;
u2 length;
u1[] bytes;
}
【tag】值为 1,表示 CONSTANT_Utf8_info
类型表。
【length】表示 u1[]
的长度,比如 length=5,则表示接下来的数据是 5 个连续的 u1 类型数据。
【bytes】u1 类型数组,长度为上面第 2 个参数 length 的值。
【注意】
在 java 代码中声明的 String 字符串最终在 class 文件中的存储格式是 CONSTANT_utf8_info
。因此一个字符串最大长度也就是 u2 所能表达的最大值 65536 个。但是需要使用2个字节来保存 null 值,因此一个字符串的最大长度为 65536-2 = 65534。
表与表之间的关系常量池内部的表中也有表与表之间的相互引用,如下图:
常量池16进制中的常量池大小
class 文件在常量池的前面使用 2个字节 的容量计数器,用来代表当前类中常量池的大小。
上图中 0017 转化为十进制是 29,也就是说常量计数器的值为 23。其中下标为 0 的常量被 JVM 留作其他特殊用途,因此当前 class 中实际的常量池大小为这个计数器的值减 1,也就是 22 个。
常量池第一个参数常量池第一个常量
上图中 0A 转化为10进制后为 10。说明对应常量池 14 种表格图中的 10,也就是 CONSTANT_Methodref_info 表(类的方法引用表)
。所以常量池中的第一个常量类型为 方法引用表
。
该方法的表结构如下:
CONSTANT_Methodref_info{
u1 tag = 10;
u2 class_index; // 指向此方法的所属类
u2 name_type_index; // 指向此方法的名称和类型
}
也就是说 0A 之后的 2 个字节指向此方法的所属类,在之后的 2 个字节表示该方法的名称和类型。如下图:
方法的类、名称、类型【0004】十进制是 4,指向常量池中的第 4 个常量。
【0011】十进制是 17,指向常量池中的第 17 个常量。
这里只解析了第一个常量,后面还有 21 个常量,也是与上面说的类似,第一个参数对应 14 种类型的下标,之后再看具体的表结构;如果u1表示一个字节,u2 表示后面 2 个字节,以此类推。
借助 javap 命令
我们可以借助 javap 命令查看 class 常量池中的内容:
javap -v Test.class
// 借助命令查看22个常量
Constant pool:
// 下标为1的指向 下标为4 和 下标为 17的常量
#1 = Methodref #4.#17 // java/lang/Object."<init>":()V
// 下标为2的指向下标为3和18的常量
#2 = Fieldref #3.#18 // ClassHexNormal.num:I
#3 = Class #19 // ClassHexNormal
#4 = Class #20 // java/lang/Object
#5 = Class #21 // java/io/Serializable
#6 = Class #22 // java/lang/Cloneable
#7 = Utf8 num
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 add
#14 = Utf8 (I)I
#15 = Utf8 SourceFile
#16 = Utf8 ClassHexNormal.java
#17 = NameAndType #9:#10 // "<init>":()V
#18 = NameAndType #7:#8 // num:I
#19 = Utf8 ClassHexNormal
#20 = Utf8 java/lang/Object
#21 = Utf8 java/io/Serializable
#22 = Utf8 java/lang/Cloneable
有上可知,下标为 1 的常量表示 Object()
方法。
4、访问标志 access_flags
紧跟在常量池之后的常量时访问标志,占用两个字节。访问标志代表类或接口的访问信息
。
比如:该 class 文件是类还是接口,是否被定义成 public,是否是 abstract,如果是了是否被声明成 final 等。
访问标志如下:
访问标志 | 值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | public类型 |
ACC_FINAL | 0x0010 | 被声明为final类型的类 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真 |
ACC_INTERFACE | 0x0200 | 标志这是一个接口类型 |
ACC_ABSTRACT | 0x0400 | 抽象类或接口类型 |
ACC_ANNOTATION | 0x2000 | 注解 |
ACC_ENUM | 0x4000 | 枚举 |
上面定义的类 ClassHexNormal.java
是一个普通 Java 类,不是接口、枚举、注解。并且被 public 修饰,但没有被声明为 final 和 abstract,因此它对应的 access_flags
为 0021 (0x0001和0x0020结合)
5、类索引、父类索引、接口索引计数器
标志后的2个字节是 类索引
;类索引后的2个字节是 父类索引
;父类索引后的2个字节是 接口索引计数器
。
6、字段表
紧跟在接口索引结合后面的就是字段表;字段表的主要功能是用来 描述类或接口中声明的变量
。
这里的字段包含类级别变量以及实例变量,不包括方法内部声明的局部变量。
【注意事项】
- 字段表集合中不会列出从父类或者父接口中继承而来的字段。
- 内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
7、方法表
字段表之后跟着的就是 方法表常量
。方法表常量
也是以一个计数器开始的,因为一个类中的方法数量是不固定的。
后面数据依次类推,这里不再举例说明。