一、什么是字节码
Java字节码是Java虚拟机所使用的指令集,是八位字节的二进制流,数据项按顺序存储在class文件中,相邻的项之间没有任何间隔,这样可以使得class文件紧凑。任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件(譬如类或接口也可以动态生成,直接送入类加载器中),也就是有一些class可以不需要以磁盘文件的形式存在。
简单的来说字节码文件即.java文件通过javac命令生成的.class文件。
jvm运行的是.class文件 而java kotlin等语言都可以通过编译器编译成.class文件
jvm会把编译的.class文件通过加载 到类加载子系统中 完成连接、初始化的步骤 成为class对象之后运行
二、Class类文件的结构
序号 | 名称 | 意思 | 类型 | 数量 |
---|---|---|---|---|
1 | magic | 魔数 | U4 | 1 |
2 | minor_version | 次板号 | U2 | 1 |
3 | major_version | 主版本号 | U2 | 1 |
4 | constant_pool_count | 常量池大小 | U2 | 1 |
5 | constant_pool | 常量池 | - | costant_pool_count - 1 |
6 | access_flags | 类的访问控制权限 | U2 | 1 |
7 | this_class | 类名 | U2 | 1 |
8 | super_class | 父类名 | U2 | 1 |
9 | interfaces_count | 接口数量 | U2 | 1 |
10 | interfaces[] | 实现的接口 | - | interfaces_count |
11 | fields_count | 成员属性数量 | U2 | 1 |
12 | field_info[] | 成员属性值 | - | fields_count |
13 | methods_count | 方法数量 | U2 | 1 |
14 | method_info[] | 方法值 | - | method_count |
15 | attributes_count | 类属性数量 | U2 | 1 |
16 | attribute_info[] | 类属性值 | - | attributes_count |
根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”
- 无符号数属于基本数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表:由多个无符号数或者其他表作为数据项构成的复合数据类型,以命名_info结尾。
接下来我们就来一一了解Class文件各组成部分,为了更直观的了解我们打开一个Class文件作为参照,因为class文件是16进制存储的,我们需要用一些工具打开,不然直接打开是乱码,我使用的是UltraEdit的软件。
- 接下来的例子都是围绕这个类的.class文件展开的
public class Test {
public static String a = "1";
public static final int b = 2;
public static void main(String[] args) {
System.out.println(a);
}
}
1. 魔数 U4
每个Class文件的头4个字节被称为魔数(Magic Number),固定值为0xCAFEBABE。魔数的作用是表示文件的类型,比如PNG图片文件、MP4可播放文件、PDF等文件基本都有自己的特殊的魔数,第三方解析器例如浏览器就可以通过魔数字符识别出文件的类型然后进行对应的逻辑解析处理。
我们这里只要记住class文件的魔数数字就是cafe babe。class的魔数的作用是判断该文件是不是一个合格class文件。
2. 次版本号 U2
占2个字节 次版本号一般全部固定为零,只有在Java2出现前被短暂使用过。但在JDK12时期,由于JDK提供的功能集已经非常庞大,有一些复杂的新特性需要以“公测”的形式放出,所以设计者重新启用了副版本号,将它用于标识“技术预览版”功能特性的支持。
3. 主版本号 U2
主版本号作用是区分jdk的版本。1.0的主版本号是44,8.0的版本号是52。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。
比如你是在jdk8上编译的,你到运行环境是jdk7的上去运行,就不会让你运行 就是用过版本号来判断的。
注意:主版本号是34是16进制 得转化成10进制 34转化成10进制就是52 说明我们用的是JDK8
4. 常量池大小 U2
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值。需要注意的是这个容量计数是从1而不是0开始。占2个字节。
注意:常量池大小是2B 转10进制为43 说明有42项常量(容量计数是从1而不是0开始)
5. 常量池(静态常量池)
我们的常量池可以看作我们的java class类的一个资源仓库(比如Java类定的 方法和变量信息),我们后面的方法、类的信息的描述信息都是通过索引去常量池中获取。常量池是表类型数据项目。
常量池主要存放两种常量: 字面量和符号引用,字面量比较接近于Java语言层面的常量概念,而符号引用则属于编译原理方面的概念。
1、字面量包含:文本字符串、final常量值、基本数据类型等
2、符号引用包含:类与接口的的全类名、字段和名称的描述符、方法名称和描述符等
常量池有三种
1、class中的常量池 静态的(我们这里分析的就是这个常量池 .class文件里的符号引用)
2、运行时常量池 动态的(加载或运行时把符号引用转化为直接引用 静态链接[加载阶段的解析过程]和动态连接[栈帧方法调用过程中])
3、字符串常量池(jdk1.6字符串常量池是包含在运行常量池中的 jdk1.7字符串常量池从永久代里的运行时常量池分离到堆里)
常量池中每一项常量都是一个表,表结构起始的第一位是个u1类型的标志位(tag取值见表中标志列),代表着当前常量属于哪种常量类型。
常量池的项目类型表:
常量池中的17种数据类型的结构总表:
- 读取第一个标志位为0A 转10进制为10 到常量池的项目类型表中查询到代表于CONSTANT_Methodref_info类型
- 再到结构总表中查询出它的结构
- 具体分析第一个常量:
tag:
u1占一个字节所以读取一个
0A 转10进制为:10 所以这个常量类型是 CONSTANT_Methodref_info
index:你对应的是哪个类的
u2占两个字节所以读取两个
00 07 转10进制为:7 calss_index符号引用值为7
index:是哪个类型的
u2占两个字节所以读取两个
00 1C 转10进制为:28 name_and_type_index符号引用值为28
#1 = Methodref #7.#28 // java/lang/Object."<init>":()V
第一个index 是7 对应#7 你对应的是哪个类的
第二个name_and_type_index 是28 对应#28
- 如何验证我们分析的类型是否正确 可以通过idea插件jclasslib或者命令行模式去验证:
在idea Terminal命令行中切到对应class目录下执行命令
javap -v Test.class
就这样依次向下分析 每分析完一个对照分析看是否正确
6. 访问标志 U2
占两个字节 读取00 21 代表public
访问标志表
7. 类名 U2
00 06 代表指向常量池 #6的地址
#6 = Class #35 // com/leetcode/test/Test
// 这个又指向#35 在常量池找到35
#35 = Utf8 com/leetcode/test/Test
最终得出类名为 com/leetcode/test/Test
8. 父类名 U2
00 07 代表指向常量池 #7的地址
#7 = Class #36 // java/lang/Object#37 = Utf8 java/lang/Object
// 这个又指向#36 在常量池找到36
#36 = Utf8 java/lang/Object
最终得出父类名为 java/lang/Object
9. 接口数量 U2
00 00 如果为0 实现接口interface[]这片区域在字节码文件中不会出现
10. 实现接口
因为接口数量为0 没有这片区域 跳过
11. 成员属性数量 U2
00 02 说明成员属性有2个
12. 成员属性值
成员属性的存储结构:
u2 access_flags 权限修饰符
u2 name_index 字段名称索引(类型名称)
u2 descriptor_index 字段描述索引(类型)
u2 attributes_count 属性表个数(属性数量)
attribute_info attribute[attribute_count](属性内容 如果属性数量为0 则没有)
attribute_info的存储结构:
u2 attribute_name_index
u4 attribute_length
u1 info[attribute_length]
我们开始分析两个成员属性值
第一个:
u2 access_flags 00 09 代表public static
u2 name_index 00 08 指向常量池#8 #8 = Utf8 a
u2 descriptor_index 00 09 指向常量池#9 #9 = Utf8 Ljava/lang/String;
u2 attributes_count 00 00(因为属性数量为0 属性内容区域没有值)
所以第一个成员属性是 public static String a
第二个:
u2 access_flags 00 19 代表public static final(public final static也为19)
u2 name_index 00 0A 指向常量池#10 #10 = Utf8 b
u2 descriptor_index 00 0B 指向常量池#11 #11 = Utf8 I (在字节码中I是int的简写 参照下面的数据类型的描述符表)
u2 attributes_count 00 01 代表有一个attribute_info
// attribute_info attribute[attribute_count] 因为属性数量为1 所以需要读取一个attribute_info[1]
attribute_info{
u2 attribute_name_index 00 0C 指向常量池#12 #12 = Utf8 ConstantValue
u4 attribute_length 00 00 00 02
u1 info[attribute_length] 因为属性数量为2 所以需要读取2个attribute_info[2] 读取2个字节 00 0D 指向常量池#13 #13 = Integer 2
}
所以第二个成员属性是 public static final int b = 2
验证我们读取的结果:
数据类型的描述符表
基本数据类型表示:
B---->byte
C---->char
D---->double
F----->float
I------>int
J------>long
S------>short
Z------>boolean
V------->void
void-------> ()v
对象类型:
String------>Ljava/lang/String;(后面有一个分号)
对于数组类型:每一个唯独都是用一个前置 [ 来表示
int[]------>[I,
String[][]------>[[Ljava.lang.String;
byte[]------>[B
String[]------>[Ljava/lang/String
二维数组就是
byte[][]------>[[B
方法的描述符规则:()V表示: (数据类型的描述符)返回值的描述符
1、比如方法为:public static void main(String[] args) {}
方法的描述符为:([Ljava/lang/String;)V
2、比如方法描述符为:([[Ljava/lang/String;, I, [Ljava/lmw/Liu;)[Ljava/lang/String
方法就为:String xxx(String[][] str, int a, Liu liu)
13. 方法数量 U2
00 03 说明方法有3个(注意要把构造方法算进去)
14. 方法值
方法值的存储结构:
u2 access_flags 权限修饰符
u2 name_index 字段名称索引(类型名称)
u2 descriptor_index 字段描述索引(类型)
u2 attributes_count 方法表个数(属性内容)
attribute_info attribute[attribute_count](属性内容 如果属性数量为0 则没有)
"attribute_info":
"Code": {
"attribute_name_index": "u2(00 09)->desc:我们属性的名称指向常量值索引的#9 位置 值为Code",
"attribute_length": "u4(00 00 00 2F)-desc:表示我们的Code属性紧接着下来的47个字节是Code的内容",
"max_stack": "u2(00 01)->desc:表示该方法的最大操作数栈的深度1",
"max_locals": "u2(00 01)->desc:表示该方法的局部变量表的个数为1",
"Code_length": "u4(00 00 00 05)->desc:指令码的长度为5",
"Code[Code_length]": "2A B4 00 02 B0 其中0x002A->对应的字节码注记符是aload_0;0xB4->getfield 获取指定类的实例域,并将其值压入栈顶;
00 02表示表示是B4指令码操作的对象指向常量池中的#2
B0表示为aretrun 返回 从当前方法返回对象引用",
"exception_table_length": "u2(00 00)->表示该方法不抛出异常,故exception_info没有异常信息",
"exception_info": {},
"attribute_count": "u2(00 02)->desc表示code属性表的属性个数为2",
"attribute_info": {
"LineNumberTable": {
"attribute_name_index": "u2(00 0A)当前属性表名称的索引指向我们的常量池#10(LineNumberTable)",
"attribute_length": "u4(00 00 00 06)当前属性表属性的字段占用6个字节是用来描述line_number_info",
"mapping_count": "u2(00 01)->desc:该方法指向的指令码和源码映射的对数 表示一对",
"line_number_infos": {
"line_number_info[0]": {
"start_pc": "u2(00 00)->desc:表示指令码的行数",
"line_number": "u2(00 0B)->desc:源码12行号"
}
},
"localVariableTable": {
"attribute_name_index": "u2(00 0B)当前属性表名称的索引指向我们的常量池#10(localVariableTable)",
"attribute_length": "u4(00 00 00 0C)当前属性表属性的字段占用12个字节用来描述local_variable_info",
"local_variable_length": "u2(00 01)->desc:表示局部变量的个数1",
"local_vabiable_infos": {
"local_vabiable_info[0]": {
"start_pc": "u2(00 00 )->desc:这个局部变量的生命周期开始的字节码偏移量",
"length:": "u2(00 05)->作用范围覆盖的长度为5",
"name_index": "u2(00 0c)->字段的名称索引指向常量池12的位置 this",
"desc_index": "u2(00 0D)局部变量的描述符号索引->指向#13的位置
"index": "u2(00 00)->desc:index是这个局部变量在栈帧局部变量表中Slot的位置"
}
}
}
}
}
}
我们开始分析三个方法值
由于方法值中attribute_info信息过于的多 这里就不展开分析了
第一个:
u2 access_flags 00 01 01 代表public
u2 name_index 00 0E 14 对应常量池的#14 #14 = Utf8 <init>(构造方法)
u2 descriptor_index 00 0F 15 对应常量池的#15 #15 = Utf8 ()V
u2 attribuyes_count 00 01
attribute_info attribute[attribute_count] 因为属性数量为1 所以需要读取一个attribute_info[1]
....
第二个:
u2 access_flags 00 09 代表public static
u2 name_index 00 15 21 对应常量池的#21 #21 = Utf8 main
u2 descriptor_index 00 16 22 对应常量池的#22 #22 = Utf8 ([Ljava/lang/String;)V
u2 attribuyes_count 00 01
attribute_info attribute[attribute_count] 因为属性数量为1 所以需要读取一个attribute_info[1]
...
第三个:
u2 access_flags 00 08 代表static
u2 name_index 00 19
u2 descriptor_index 00 0F
u2 attribuyes_count 00 01
attribute_info attribute[attribute_count] 因为属性数量为1 所以需要读取一个attribute_info[1]
...
最后也是可以通过命令行打印出来的信息来验证我们读取的结果
15.类属性数量 U2
读取两个 00 01
16.类属性值
类属性值的存储结构:
u2 attribute_name_index
u4 name_index_length
u2 sourcefile_index
按照数据结构读取
u2 attribute_name_index 00 1A 26对应常量池的#26 #26 = Utf8 SourceFile
u4 name_index_length 00 00 00 02
u2 sourcefile_index 00 1B 27对应常量池的#27 #27 = Utf8 Test.java
到此刚好读取完成