目录
一. 前言
二. class文件结构
2.1. 文件格式
2.2. 魔数与版本号
2.3. 常量池
2.4. 访问标志
2.5. 类索引、父类索引和接口索引集合
2.6. 字段表集合
2.7. 方法表集合
2.8. 属性表集合
2.8.1. Code 属性表
2.8.2. Exceptions 属性
2.8.3. LineNumberTable 属性
2.8.4. LocalVariableTable 属性
2.8.5. SourceFile 属性
2.8.6. ConstantValue 属性
2.8.7. InnerClass 属性
2.8.8. Deprecated 属性和 Synthetic 属性
2.8.9. StackMapTable 属性
2.8.10. Signature 属性
2.8.11. BootstrapMethods 属性
三. 实例分析
3.1. 示例源码
3.2. 魔数
3.2. 版本号
3.3. 常量池
3.3.1. 常量池第 1 个常量
3.3.2. 常量池第 2 个常量
3.3.3. 常量池中第 3~4 常量
3.3.4. 常量池中第 5~14 及 17、18常量
3.3.5. 常量池第 15、16 个常量
3.4. 访问标志
3.5. 类索引、父类索引
3.6. 字段表集合
3.7. 方法表集合
3.7.1. 第 1 个方法表
3.7.2. 第 2 个方法表
3.8. Code 属性表与 LineNumberTable 属性表
3.9. SourceFile 属性
一. 前言
我们平时在DOS界面中往往需要先运行 javac 命令,这个命令的直接结果就是产生相应的class文件,然后基于这个class文件才可以真正运行程序得到结果。当然,这是Java虚拟机的功劳,那么是不是Java虚拟机只能编译 .java 的源文件呢?答案是否定的。时至今日,Java虚拟机已经实现了语言无关性的特点。而实现语言无关性的基础是虚拟机和字节码的存储格式,Java虚拟机已经不和包括Java语言在内的任何语言绑定。它只与“class”文件这种特定的二进制文件相关联。在class文件中包含了Java虚拟机指令集和符号表以及若干辅助信息。可以很容易想到Java(本质上不是Java语言本身的平台无关性,而是其底层的Java虚拟机的平台无关性使然。)的跨平台,因为任何一门功能性语言都可以表示为能被Java虚拟机接受的有效的class文件。比如,除了Java虚拟机可以将Java源文件直接编译为class文件外,使用JRuby等其他语言的编译器一样可以把程序代码编译成class文件,由此可见,Java虚拟机并不关心class文件是由何种语言编译来的。
二. class文件结构
下面的图是一字排开形式,都是平级关系,由于图片排开太长,所以分行显示,如下面这样:
class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构只有两种数据结构,即无符号数和表,解析class文件全是以这两个数据结构为基础。
无符号数:属于基本的数据类型,由1字节、2字节、4字节、8字节分别用u1、u2、u3、u8表示,可以用来描述数字、索引引用、数量值或者UTF-8编码构成字符串值。
表:是由多个无符号数或者表构成的复合数据结构,习惯以“_info”结尾class文件本质也可以看作一张表。
2.1. 文件格式
class文件格式严格按照下表的方式进行排列构成:
类型 | 名称 | 含义 | 数量 |
---|---|---|---|
u4 | magic | 魔数 | 1 |
u2 | minor_version | 副版本号 | 1 |
u2 | major_version | 主版本号 | 1 |
u2 | constant_pool_count | 常量池计数器 | 1 |
cp_info | constant_pool | 常量池数据区(常量池表) | constant_pool_count -1 |
u2 | access_flags | 访问标志(类的访问控制权限) | 1 |
u2 | this_class | 类索引 | 1 |
u2 | super_class | 父类索引 | 1 |
u2 | interfaces_count | 接口计数器 | 1 |
u2 | interfaces | 接口信息数据区(接口表) | interfaces_count |
u2 | fields_count | 字段计数器 | 1 |
field_info | fields | 字段信息数据区(字段表) | fields_count |
u2 | methods_count | 方法计数器 | 1 |
method_info | methods | 方法信息数据区(方法表) | methods_count |
u2 | attributes_count | 附加属性计数器 | 1 |
attribute_info | attributes | 附加属性信息数据区(附加属性表) | attributes_count |
2.2. 魔数与版本号
魔数固定为 0xCAFEBABE,4个字节。魔数的唯一作用在于确定这个class文件是否是Java虚拟机接受的class文件。如gif和jpeg等在文件头中都存在魔术,使用魔术而不是使用扩展名是基于安全性考虑的——扩展名可以随意被改变。
紧接着魔数的4个字节是class文件版本号:版本号又分为副版本号和主版本号。其中前两个字节用于表示副版本号,后两个字节用于表示主版本号。版本号是随着JDK版本的不同而表示不同的版本范围的。如果class文件的版本号超过虚拟机版本,将被拒绝执行。
2.3. 常量池
在版本号之后,则是常量池入口,常量池在class文件中的作用非常重要,就是class文件中的资源仓库,通常也是占用class文件空间最大的数据项目之一。由于常量池长度不是一定的,所以在常量池的入口处放置了一个两个字节的常量池计数器来记录常量池的容量有多大。
常量池计数器:记录的是常量池数据区中的常量池项 cp_info 的数量。
1. 从1开始计数,第一个有用常量池项为1,所以常量池项的索引也是从1开始;
2. 至于索引为0的数据空间,class文件规范是如此定义的。
在常量池中主要存放字面量和符号引用。字面量比较接近Java语言层面的常量概念,比如文本字符串、声明为final的常量值等。符号引用则主要包括三类常量:
1. 类和接口的全限定名;
2. 字段的名称和描述符;
3. 方法的名称和描述符。
// 常量池项 (cp_info) 的结构
cp_info{
tag:xx
info[]:xx
}
tag:类型,用标记下面info数组的数据类型。
info[]:若干个字节构成数组。
常量 | 常量说明 | 项目 | 类型 | 描述 |
---|---|---|---|---|
CONSTANT_Utf8_info | 表示字符串常量的值。 (字面量型结构体) | tag标志 | u1 | 值为1 |
length | u2 | UTF-8编码的字符串占用的字节数 | ||
bytes | u1 | 长度为length的UTF-8编码的字符串 | ||
CONSTANT_Integer_info | 表示4字节(int)的数值常量。 (字面量型结构体) | tag标志 | u1 | 值为3 |
bytes | u4 | 按照高位在前存储的int值 | ||
CONSTANT_Float_info | 表示4字节(float)的数值常量。 (字面量型结构体) | tag标志 | u1 | 值为4 |
bytes | u4 | 按照高位在前存储的float值 | ||
CONSTANT_Long_info | 表示8字节(long)的数值常量。 (字面量型结构体) | tag标志 | u1 | 值为5 |
bytes | u8 | 按照高位在前存储的long值 | ||
CONSTANT_Double_info | 表示8字节(double)的数值常量。 (字面量型结构体) | tag标志 | u1 | 值为6 |
bytes | u8 | 按照高位在前存储的double值 | ||
CONSTANT_Class_info | 表示类或接口的完全限定名。 (引用型结构体) | tag标志 | u1 | 值为7 |
index | u2 | 指向全限定名常量项的索引 | ||
CONSTANT_String_info | 表示java.lang.String类型的常量对象。 (引用型结构体) | tag标志 | u1 | 值为8 |
index | u2 | 指向字符串字面量的索引 | ||
CONSTANT_Fieldref_info | 表示类中的字段。 (引用型结构体) | tag标志 | u1 | 值为9 |
index | u2 | 指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 | ||
index | u2 | 指向字段描述符CONSTANT_NameAndType的索引项 | ||
CONSTANT_Methodref_info | 表示类中的方法。 (引用型结构体) | tag标志 | u1 | 值为10 |
index | u2 | 指向声明方法的类描述符CONSTANT_Class_info的索引项 | ||
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | ||
CONSTANT_InterfaceMethodref _info | 表示类中的方法。 (引用型结构体) | tag标志 | u1 | 值为11 |
index | u2 | 指向声明方法的接口描述符CONSTANT_Class_info的索引项 | ||
index | u2 | 指向名称及类型描述符CONSTANT_NameAndType的索引项 | ||
CONSTANT_NameAndType_info | 表示字段或方法的名称和类型。 (引用型结构体) | tag标志 | u1 | 值为12 |
index | u2 | 指向该字段或方法名称常量项的索引 | ||
index | u2 | 指向该字段或方法描述符常量项的索引 | ||
CONSTANT_MethodHandle_info | 表示方法句柄。 (引用型结构体) | tag标志 | u1 | 值为15 |
CONSTANT_MethodType_info | 表示方法类型。 (引用型结构体) | tag标志 | u1 | 值为16 |
CONSTANT_Dynamic_info | 表示一个动态计算常量。 (引用型结构体) | tag标志 | u1 | 值为17 |
CONSTANT_InvokeDynamic _info | 用于表示invokedynamic指令所使用到的引导方法(Bootstrap Method)、引导方法使用到动态调用名称(Dynamic Invocation Name)、参数和请求返回类型 | tag标志 | u1 | 值为18 |
CONSTANT_Module_info | 表示一个模块 | tag标志 | u1 | 值为19 |
CONSTANT_Package_info | 表示一个模块中开放或者导出的包 | tag标志 | u1 | 值为20 |
细化了的常量池的结构会是类似下图所示的样子:
在JVM规范中,每个字段或者变量都有描述信息,描述信息的主要作用是 数据类型、方法参数列表、返回值类型等。基本参数类型和void类型都是用一个大写的字符来表示,对象类型是通过一个大写L加全类名表示,这么做的好处就是在保证JVM能读懂class文件的情况下尽量的压缩class文件体积。
基本数据类型表示:
字符 | 类型 | 字符 | 类型 |
---|---|---|---|
B | byte | C | char |
D | double | F | float |
I | int | J | long |
S | short | Z | boolean |
V | void |
对象类型:前面加一个L,然后把.改为/,String------>Ljava/lang/String;(后面有一个分号)。
数组类型:每一个维度都是用一个前置 [ 来表示,比如: int[] ------>[ I,String [][]------>[[Ljava.lang.String;(后面有一个分号)。
用描述符来描述方法:先参数列表,后返回值的格式,参数列表按照严格的顺序放在()中
比如源码 String getUserByIdAndName(int id, String name) 的方法描述符号为(I,Ljava/lang/String;)Ljava/lang/String;(后面有一个分号)。
2.4. 访问标志
常量池之后的数据结构是访问标志(access_flags),这个标志主要用于识别一些类或者接口层次的访问信息,主要包括:这个Class是类还是接口、是否定义public、是否定义abstract类型;如果是类的话是否被声明为final等。
类的标志访问如下:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类能设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志位为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 是否为这个类是由编译器自动产生,并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举类 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
字段的标志访问如下:
方法的标志访问如下:
两个访问修饰符直接通过位运算来实现的。例如:现在有一个class文件访问标识为00 21。那么实际它代表的是0x0021 = 0x0020 位运算 0x0001,即它代表这个class的访问权限是ACC_PUBLIC和ACC_SUPER。
2.5. 类索引、父类索引和接口索引集合
类索引和父类索引都是一个两个字节的数据,而接口索引是一组两字节的数据集合,class文件中由这三项数据来确定该类型的继承关系,类索引用于确定该类的全限定类名,父类索引用于确定该类父类的全限定类名。由于Java语言特性不支持多类继承,所以父类索引只有一个,接口索引集合用于描述这类实现了哪些接口。
2.6. 字段表集合
字段表用于描述接口或者类中声明的变量。字段包括类级变量和实例级变量,但是不包括方法内部声明的局部变量(这些变量是存储在Java虚拟机栈中的局部变量表中的)。自然,描述一个字段的信息包括:字段的作用域(public、protected、private)、实例变量与否(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本数据类型、对象、数组)、字段名称。
字段的信息也被存放在一张表中,其字段表包括三种类型:
1. u2类型访问标志(access_flags),其访问标志在access_flags中。
2. u2类型的name_index(字段的简单名称)。
3. u2类型的描述符(descriptor_index)。
字段表格式:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
2.7. 方法表集合
JVM中堆方法表的描述与字段表是一致的,包括了:访问标志、名称索引、描述符索引、属性表集合。方法表的结构与字段表是一致的,区别在于访问标志的不同。在方法中不能用volatile和transient关键字修饰,所以这两个标志不能用在方法表中。在方法中添加了字段不能使用的访问标志,比如方法可以使用synchronized、native、strictfp、abstract关键字修饰,所以在方法表中就增加了相应的访问标志。
注意:如果父类方法没有在子类中重写,那么在方法中不会自动出现来自父类的方法信息。同样的,有可能添加编译器自动增加的方法,比如方法。
方法表的结构如下:
2.8. 属性表集合
前面的class文件、字段表和方法表都可以携带自己的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。在属性表中没有类似class文件的数据项目类型和顺序的严格要求,只要新的属性不与现有的属性名重复,任何人都可以向属性表中写入自己定义的属性信息。
2.8.1. Code 属性表
Java程序方法体中的代码经过 javac 编译最终编译成的字节码指令就保存在Code属性中。但是并非所有的方法表都必须存在这个属性。Code属性是class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code)和元数据(Metadata,包括类、字段、方法定义及其其他信息)两部分,那么在整个class文件中,Code属性用于描述代码,所有其他的数据项目都用于描述元数据。
Code属性表的结构:
类型 | 名称 | 数量 | 说明 |
---|---|---|---|
u2 | attribute_name_index | 1 | 指向CONSTANT_Utf8_info型常量的索引,此常量固定为“Code”,代表该属性的属性名称 |
u4 | attribute_length | 1 | 表示属性长度 |
u2 | max_stack | 1 | 表示操作数栈深度的最大值 |
u2 | max_locals | 1 | 表示局部变量表所需的存储空间,单位是变量槽(虚拟机为局部变量分配内存所使用的最小单位) |
u1 | code_length | 1 | 表示字节码长度 |
u2 | code | code_length | 用于存储字节码指令。用来存储java源文件编译后生成的字节码指令 |
u2 | exception_table_length | 1 | 表示异常处理表集合长度 |
exception_info | exception_table | exception_table_length | 表示异常处理表集合,异常处理表并不是Code必须部分! |
u2 | attributes_count | 1 | |
attribute_info | battributes | attribute_count |
2.8.2. Exceptions 属性
Exceptions 属性的作用是列举出方法中可能抛出的受检异常(Checked Exception),也就是描述throws 后的列举的异常。
2.8.3. LineNumberTable 属性
LineNumberTable属性用于描述Java源码行号与上传字节码行号之间的对应关系,并不是运行的必须属性,但会默认生成到class文件中,主要作用就是抛出异常时会显示行号,以及调试程序时可以根据源码行号进行设置断点。
LineNumberTable的属性结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
line_number_table是一个 l类型为line_number_info的集合,包含start_pc和line_number两个属性都是占两个字节,前者是字节码行号,后者是java源码行号。
2.8.4. LocalVariableTable 属性
用于描述栈帧中局部变量表中的变量与Java源码中定义的变量的之间的关系。也不是必须的属性。如果没有这个属性,产生的直接影响就是当别人引用这个方法的时候,所有的参数名称都会丢失,IDE将会使用诸如 args0、args1 之类的参数进行显示。自然,当调试程序的时候,显示的参数名称是不可知的。
2.8.5. SourceFile 属性
用于记录这个class文件的源码文件名称,当类名和文件名不一致时抛出异常。如果不使用这个属性,那么当抛出异常的时候,堆栈中将不会显示出错代码所属的文件名。
SourceFile属性的结构为:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | sourcefile_index | 1 |
2.8.6. ConstantValue 属性
ConstantValue 属性作用是通知虚拟机自动为静态变量赋值。要注意的是,只有被 static 关键字修饰的变量才可以使用这个属性(类变量)。对于非类变量,初始化是在方法中进行的;对于类变量可以选择两种方式进行变量的初始化:一是在类构造器方法中使用;二是是ConstantValue属性。目前Sun HotSpot的选择原则是:如果一个变量同时使用static和final关键字修饰,并且这个变量是基本数据类型或者 java.lang.String 类型的话,就使用ConstantValue属性进行初始化。如果没有被 final 修饰或者并非是基本数据类型,那么将会选择使用方法进行初始化。
2.8.7. InnerClass 属性
这个属性主要用于记录内部类与宿主类之间的关联关系。
2.8.8. Deprecated 属性和 Synthetic 属性
这两个属性都属于标志类型的布尔属性,只存在有没有的区别。
Deprecated 属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,可以通过注解 @deprecated 实现。
Synthetic 属性代表此字段并不是由Java源码产生的,而是通过编译器自行添加的。
2.8.9. StackMapTable 属性
该属性的目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
2.8.10. Signature 属性
这个属性是专门用来记录泛型类型的,因为在Java语言采用的是擦除法实现的泛型,在字节码(Code属性)中,泛型信息编译之后会被擦除。擦除法的优点是能够节省泛型所占的内存空间,缺点是在运行期间无法通过反射得到泛型信息,而Signature属性则弥补了这一缺陷。现在的Java反射API已经能够得到泛型信息,功劳就在于这个属性。
2.8.11. BootstrapMethods 属性
这个属性用于保存 invokedynamic 指令引用的引导方法限定符。该指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法。
三. 实例分析
3.1. 示例源码
示例代码:
public class Test {
private int m;
public int inc(){
return m + 1;
}
}
编译后的 class文件(使用16进制的方式来打开):
cafe babe 0000 0034 0013 0a00 0400 0f09
0003 0010 0700 1107 0012 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 0369 6e63
0100 0328 2949 0100 0a53 6f75 7263 6546
696c 6501 0009 5465 7374 2e6a 6176 610c
0007 0008 0c00 0500 0601 0004 5465 7374
0100 106a 6176 612f 6c61 6e67 2f4f 626a
6563 7400 2100 0300 0400 0000 0100 0200
0500 0600 0000 0200 0100 0700 0800 0100
0900 0000 1d00 0100 0100 0000 052a b700
01b1 0000 0001 000a 0000 0006 0001 0000
0001 0001 000b 000c 0001 0009 0000 001f
0002 0001 0000 0007 2ab4 0002 0460 ac00
0000 0100 0a00 0000 0600 0100 0000 0400
0100 0d00 0000 0200 0e
3.2. 魔数
ca fe ba be // 魔数值0xCAFEBABE
上述代码可以看出 class文件的魔术值为0xCAFEBABE。
MagicNumber 为每个class文件的头4个字节,唯一作用就是用于判断这个文件是否为一个能被虚拟机接受的 class文件。为什么使用魔数不使用扩展名进行判断?(出于安全考虑,扩展名可以随意修改,魔术值只要没有被广泛采用就不会引起混淆)。
3.2. 版本号
00 00 00 34 // 前两位为次版本号,后两位为主版本号
版本号为魔数后4个字节,前两个字节为副版本号,后两个字节为主版本号,0034十进制是52表示Java8,关于副版本号,在JDK1.2时短暂使用过,从JDK1.2到JDK12之前副版本号均为零,未被使用过。
3.3. 常量池
00 13 // 00 13表示常量中常量的数量为18
3.3.1. 常量池第 1 个常量
观察例子中常量池第一个常量,它的标志位为0x0a,根据2.3 章节表可得知这个常量类型为CONSTANT_Methoddref_info,CONSTANT_Methoddref_info 类型结构为:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为10 |
u2 | index | 1 | 指向方法的类描述符CONSTANT_Class_info索引项 |
u2 | index | 1 | 指向名称及类型描述符CONTANT_Name_AndType索引项 |
结合字节码分析,该类型一共占5个字节:
第一个占1个字节,用于区分索引类型;
第二个占2个字节,用于指向该类型中的CONSTANT_Class_info在常量池中的索引;
第三个占2个字节,用于指向该类型中的CONSTANT_NameAndType在常量池中的索引。
0a // 0a表示第 1 个常量类型为CONSTANT_Methoddref_info 长度为5个字节(包含标志位)
00 04 // 表示声明方法的类的字段 CONSTANT_Class_info的索引项在常量池第4项常量
00 0f // 表示名称及类型字段 CONSTANT_NameAndType的索引项在常量池的第 15 项常量
3.3.2. 常量池第 2 个常量
例子中常量池第二个常量,它的标志位为0x09,根据2.3 章节表可得知常量类型CONSTANT_Fieldref_info 的类型结构为:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为9 |
u2 | index | 1 | 指向方法的类描述符CONSTANT_Class_info索引项 |
u2 | index | 1 | 指向名称及类型描述符CONTANT_Name_AndType索引项 |
结合字节码分析,该类型一共占5个字节:
第一个占1个字节,用于区分索引类型;
第二个占2个字节,用于指向该类型中的CONSTANT_Class_info在常量池中的索引;
第三个占2个字节,用于指向该类型中的CONSTANT_NameAndType在常量池中的索引。
09 // 表示第 2 个常量类型为 CONSTANT_Fieldref_info 长度为5个字节(包含标志位)
00 03 // 表示声明方法的类的字段 CONSTANT_Class_info 的索引项在常量池第 3 项常量
00 10 // 表示字段CONSTANT_Name-AndType的索引在常量池的第 16 项
3.3.3. 常量池中第 3~4 常量
例子中第3个和第4个常量类型一样,它的标志位都为0x07,根据2.3 章节表可得知常量类型为CONTANT_Class_info,类型结构为:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为7 |
u2 | index | 1 | 指向全限定名常量的索引 |
结合字节码分析,该类型一共占3个字节:
第一个占1个字节,用于区分索引类型;
第二个占2个字节,用于指向全限定名常量项的索引。
07 // 表示第 3 个常量类型为 CONTANT_Class_info 长度为 3 个字节(包含标志位)
00 11 // 表示全限定名存在常量池的第 17 项常量
07 // 表示第 4 个常量类型为 CONTANT_Class_info 长度为 3 个字节(包含标志位)
00 12 // 表示全限定名存在常量池的第 18 项常量
3.3.4. 常量池中第 5~14 及 17、18常量
第5到14以及17、18常量,它们标志位都是0x01,根据2.3 章节表可得知常量类型为CONTANT_Utf8_info, 类型结构为 CONTANT_Class_info,类型结构为:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为1 |
u2 | length | 1 | UTF-8编码的字符串占用了字节数 |
u1 | bytes | length | 长度为length的UTF-8编码的字符串 |
结合字节码分析:
第一个占1个字节,用于区分索引类型;
第二个占2个字节,用于描述该常量占用多少(length)字节数;
第三个占length个字节数,用于存储utf-8编码的字符串 。
01 // 表示第 5 个常量类型为 CONTANT_Utf8_info 长度为 4 个字节(包含标志位)
00 01 // 表示utf-8编码占用了字节数为 1
6d // 6d 十进制为109 utf-8对应 m
01 // 表示第 6 个常量类型为 CONTANT_Utf8_info
00 01 // 表示长度为 1
49 // 49 十进制为 73 utf8对应 I
01 // 第 7 个常量为CONTANT_Utf8_info
00 06 // 常量长度为 6
3c 69 6e 69 74 3e // 一一对应 '<init>'
<!-- 下面utf全部简略的描述 -->
01 // 第 8 个常量
00 03 // 长度为 3
28 29 56 // utf对应 ()V
01 // 第 9 个常量
00 04 // 长度为 4
43 6f 64 65 // Code
01 // 第 10 个变量
00 0f // 长度为 15
4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 // LineNumberTable
01 // 第 11 个变量
00 03 // 长度为3
69 6e 63 // inc
01 // 第 12 个变量
00 03 // 长度为3
28 29 49 // ()I
01 // 第 13 个变量
00 0a // 长度为10
53 6f 75 72 63 65 46 69 6c 65 // SourceFile
01 // 第 14 个变量
00 09 // 长度为9
54 65 73 74 2e 6a 61 76 61 // Test.java
······
01 // 第 17 个常量为 utf-8
00 04 // 长度为 4
54 65 73 74 // Test
01 // 第 18 个常量为 utf-8
00 10 // 长度为 16
6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74 // java/lang/Object
3.3.5. 常量池第 15、16 个常量
第15、16个常量,它们的标志为都是0x0c,根据2.3 章节表可得知常量类型为CONSTANT_NameAndType_info,类型结构如下:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u1 | tag | 1 | 值为12 |
u2 | index | 1 | 指向该字段或方法名称常量项索引 |
u2 | index | 1 | 指向指向该字段或方法描述常量项索引 |
结合字节码分析,该类型一共占5个字节:
第一个占1个字节,用于区分索引类型;
第二个占2个字节,指向该字段或方法名称常量项索引;
第三个占2个字节,指向指向该字段或方法描述常量项索引。
0c // 表示第 15 个常量量类型为 CONSTANT_NameAndType_info 长度为5个字节(包含标志位)
00 07 // 表示该字段的表示的字段或方法的名称为常量池的第7个常量
00 08 // 表示该字段表示的字段或方法的描述为位常量池的第8个常量
0c // 表示第 16 个常量量类型为 CONSTANT_NameAndType_info 长度为5个字节(包含标志位)
00 05 // 表示该字段的表示的字段或方法的名称为常量池的第5个常量
00 06 // 表示该字段表示的字段或方法的描述为位常量池的第6个常量
到这常量池已经全部解析完成: 可以使用 javap -verbose 命令输出常量表,对常量表进行验证:
3.4. 访问标志
0021 根据2.3 章节表可得知该类使用 invokespecial 字节码指令的新语义,且被public修饰。
00 21 // access_flags 标志位占用 2个字节位,0021表示被public修饰符修饰,且使用invokespecial字节码指令的新语义
3.5. 类索引、父类索引
00 03 // this_class 类索引,占用 2 个字节,表示在类名称存放在常量池第 3 项常量中
00 04 // super_class 父类索引,占用 2 个字节,表示父类名称存放在常量池第 4 项常量中
00 // interface_count 占用 1 个字节,表示实现接口的数量为0
00 // interfaces 接口为空
3.6. 字段表集合
<!-- 字段表集合 -->
00 01 // fields_count 表示只有 1 个字段表数据 占用 2 个字节
00 02 // access_flags 表示字段修饰符为private 占用 2 个字节
00 05 // name_index 表示字段名称存放在常量池第 5 项常量 占用 2 个字节
00 06 // descriptor_index 表示字段方法的描述存放在常量池的第 6 项常量 占用 2 个字节
00 // attributes_count 属性表数量为0 占用 2 个字节
00 // attributes 表示属性表集合为空
3.7. 方法表集合
<!-- 方法表集合 -->
00 02 // methods_count 表示表示方法表集合中包含 2 个方法
3.7.1. 第 1 个方法表
<!-- 第一个方法表 -->
00 01 // access_flags 表示方法修饰符为public 占用 2 个字节
00 07 // name_index 方法名称存放在常量池第 7 个 占用 2 个字节
00 08 // descriptor_index 表示方法描述存放在常量池第 8 个 占用 2 个字节
00 01 // attributes_count 表示此方法属性表集合中有 1 项属性 占用 2 个字节
<!-- attribute_info (code)-->
00 09 // attribute_name_index 表示属性存放在常量池第 9 项 占用 2 个字节
00 00 00 1d // attribute_lenth 表示该属性的长度 占用 4 个字节
00 01 // max_stack 操作数栈的最大深度 占用 2 个字节
00 01 // max_locals 局部变量表所需的存储空间 占用两个字节
00 00 00 05 // code_length 表示字节码长度,占用 4 个字节
2a b7 00 01 b1 // code 用于存储编译后的字节码指令 占用code_length 个字节
00 00 // exception_table_length 异常表长度 占用 2 个字节
00 01 // code属性中的 attributes_count 占用两个字节
00 0a // attribute_name_index 属性名称在常量池的第10项常量
00 00 00 06 // LineNumber类型的 attribute_length 属性长度 占用 4 个字节
00 01 // line_number_table_length 表示有几个line_number_info类型的数据 占用 2 个字节
<!-- line_number_info -->
00 00 // start_pc 字节码行号
00 01 // line_number java源码行号
3.7.2. 第 2 个方法表
<!-- 第二个方法表 -->
00 01 // access_flags 表示方法修饰符为public 占用 2 个字节
00 0b // name_index 方法名称存放在常量池第 11 个 占用 2 个字节
00 0c // descriptor_index 表示方法描述存放在常量池第 12 个 占用 2 个字节
00 01 // attributes_count 表示此方法属性表集合中有 1 项属性 占用 2 个字节
<!-- attribute_info (code) -->
00 09 // attribute_name_index 表示属性存放在常量池第 9 项 占用 2 个字节
00 00 00 1f // attribute_lenth 表示该属性的长度 占用 4 个字节
00 02 // max_stack 操作数栈的最大深度 占用 2 个字节
00 01 // max_locals 局部变量表所需的存储空间 占用 2 个字节
00 00 00 07 // code_length 表示字节码的长度 占用 4 个字节
2a b4 00 02 04 60 ac// code 用于存储编译后的字节码指令 占用code_length个字节
00 00 // exception_table_length 异常表长度 占用 2 个字节
00 01 // cod属性中的 attributes_count 表示有几个属性 占用两个字节
00 0a // attribute_name_index 属性名在常量池中的索引 表示是常量池中第 10 个索引 占用 2 个字节
00 00 00 06 // LineNumber类型的 attribute_length 属性长度占用 4 个字节
00 01 // line_number_table_length 表示有几个 line_number_info类型的数据 站哟 两个字节
<!-- line_number_info -->
00 00 // start_pc 字节码行号
00 04 // line_number java源码行号
3.8. Code 属性表与 LineNumberTable 属性表
<!-- 第一个方法 attribute_info (code)-->
00 09 // attribute_name_index 表示属性存放在常量池第 9 项 占用 2 个字节
00 00 00 1d // attribute_lenth 表示该属性的长度 占用 4 个字节
00 01 // max_stack 操作数栈的最大深度 占用 2 个字节
00 01 // max_locals 局部变量表所需的存储空间 占用两个字节
00 00 00 05 // code_length 表示字节码长度,占用 4 个字节
2a b7 00 01 b1 // code 用于存储编译后的字节码指令 占用code_length 个字节
00 00 // exception_table_length 异常表长度 占用 2 个字节
00 01 // code属性中的 attributes_count 占用两个字节
00 0a // attribute_name_index 属性名称在常量池的第10项常量
00 00 00 06 // LineNumber类型的 attribute_length 属性长度 占用 4 个字节
00 01 // line_number_table_length 表示有几个line_number_info类型的数据 占用 2 个字节
<!-- line_number_info -->
00 00 // start_pc 字节码行号
00 01 // line_number java源码行号
<!--第二个方法 attribute_info (code) -->
00 09 // attribute_name_index 表示属性存放在常量池第 9 项 占用 2 个字节
00 00 00 1f // attribute_lenth 表示该属性的长度 占用 4 个字节
00 02 // max_stack 操作数栈的最大深度 占用 2 个字节
00 01 // max_locals 局部变量表所需的存储空间 占用 2 个字节
00 00 00 07 // code_length 表示字节码的长度 占用 4 个字节
2a b4 00 02 04 60 ac// code 用于存储编译后的字节码指令 占用code_length个字节
00 00 // exception_table_length 异常表长度 占用 2 个字节
00 01 // cod属性中的 attributes_count 表示有几个属性 占用两个字节
00 0a // attribute_name_index 属性名在常量池中的索引 表示是常量池中第 10 个索引 占用 2 个字节
00 00 00 06 // LineNumber类型的 attribute_length 属性长度占用 4 个字节
00 01 // line_number_table_length 表示有几个 line_number_info类型的数据 站哟 两个字节
<!-- line_number_info -->
00 00 // start_pc 字节码行号
00 04 // line_number java源码行号
3.9. SourceFile 属性
<!-- 属性表 -->
00 01 // attributes_count 表示有几个属性 占用两个字符
<!-- attribute_info -->
00 0d // attribute_name_index 属性名称在常量池中的索引 占用 2 个字节
00 00 00 02 // attribute_length 属性长度 占用 4 个字节
00 0e // sourcefile_index 资源文件名称在常量池中的索引
到此为止,class文件全部解析完成,读者也可以使用该Java源代码自己进行编译按照该思路进行分析,相信你们一定也会有所收获!