作为一个大龄程序员,从开始学习java的时候就开始接触 class文件 ,作为一个拿来即用主义的人,仅需要知道怎么跑起程序即可完成所有工作,从来没有想到看什么是 字节码 ,无奈 Java 太内卷了,只能尝试花了一天,去看懂字节码。
啥是class文件
Class文件格式采用一种类似于 C语言 结构体的方式进行数据存储,这种结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和 8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF -8编码构成 字符串 值。
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class 文件本质上就是一张表。由于表没有固定长度,所以通常会在其前面加上个数说明。
为了避免与类和类实例等字段的混淆,描述类文件格式的结构的内容被称为项。连续的项是按顺序存储在类文件中,且没有填充或对齐。
class文件结构与每项占用的字节长度
Class File {
u4 magic; // 魔数 ,标识为Class文件
u2 minor_version; //副版本号(小版本)
u2 major_version; //主版本号(大版本)
u2 constant_pool_count; //常量池 计数器
cp_info constant_pool[constant_pool_count-1]; //常量池表
u2 access_flags; //访问标识
u2 this_class; //类索引
u2 super_class; //父类索引
u2 interfaces_count; //接口计数器
u2 interfaces[interfaces_count]; //接口索引集合
u2 fields_count; //字段计数器
field_info fields[fields_count]; //字段表
u2 methods_count; //方法计数器
method_info methods[methods_count]; //方法表
u2 attributes_count; //属性计数器
attribute_info attributes[attributes_count]; //属性表
}
从 javac 对源码进行编译后得到的class文件,打开后类似于乱码的文件,实际上内容是按照class的结构以每一项类似数组按顺序排列。
例如 [u4 magic][u2 minor_version][u2 major_version][…][…][…][…][…][…][…]
参考ClassFile里面的结构,根据字节码文件按照对应的字节数进行翻译。
通过javap -v -p .class文件也能得到字节码翻译后的文件
开始痛苦之旅
魔数(2字节)
提供标识类文件格式的魔术数字;它的值为0xCAFEBABE。
主版本号(2字节)与次版本号(2字节)
决定class文件的版本号,通常以主版本号.次版本号作为class文件的版本,高版本的字节码文件不能在低版本的虚拟机中运行,Java8对应的版本号为52.0,16 进制 34代表是52。
常量池计数器(2字节)
常量池计数器值为常量池长度 + 1 ,即常量池长度为10 ,那么常量池计数器值为11。该常量池计数器 26即常量池计数器为38。
常量池表
常量池主要存放两大常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值。而符号引用则属于编译原理方面的概念。
常量池中每一项常量都是一个表 ,开始的第一位是一个 u1 类型的标志位 【tag】 来表示常量类型。
#常量池结构
cp_info {
u1 tag;
u1 info[];
}
tag代表的含义:
类型 | 标志(tag) | 描述 |
CONSTANT_utf8_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 | 表示一个动态方法调用点 |
常量池第一项中0A等于10进制里面的10,根据tag对照表中属于 CONSTANT_Methodref_info结构 一共5个字节。
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
{
tag:CONSTANT_Methodref_info ,
class_index:6 ,
name_and_type_index:24
}
常量池表中第7项中,tag = 01 ,length = 8 ,bytes长度 = 8 , 总长度 1 + 2 + 8 = 11
每项常量池都以这种方式去翻译得出下表,一共37个项,证明常量池表长度 + 1 = 常量池计数器的值。
#1 = Methodref #6.#24 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#25 // simple02/ByteCodeTest.fieldInt:I
#3 = Fieldref #26.#27 // java/lang/System.out:Ljava/io/PrintStream;
#4 = Methodref #28.#29 // java/io/PrintStream.println:(I)V
#5 = Class #30 // simple02/ByteCodeTest
#6 = Class #31 // java/lang/Object
#7 = Utf8 fieldInt
#8 = Utf8 I
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lsimple02/ByteCodeTest;
#16 = Utf8 method01
#17 = Utf8 ()I
#18 = Utf8 main
#19 = Utf8 ([Ljava/lang/String;)V
#20 = Utf8 args
#21 = Utf8 [Ljava/lang/String;
#22 = Utf8 Source File
#23 = Utf8 ByteCodeTest.java
#24 = NameAndType #9:#10 // "<init>":()V
#25 = NameAndType #7:#8 // fieldInt:I
#26 = Class #32 // java/lang/System
#27 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#28 = Class #35 // java/io/PrintStream
#29 = NameAndType #36:#37 // println:(I)V
#30 = Utf8 simple02/ByteCodeTest
#31 = Utf8 java /lang/Object
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 java/io/PrintStream
#36 = Utf8 println
#37 = Utf8 (I)V
此时,正常情况需要停下休息,去治疗一下眼睛了。
访问权限(2字节)
访问标识 用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为 public类型,是否定义为 abstract 类型;如果是类的话,是否被声明为final等。
类索引(2字节)
该索引指向的字面量格式文件的全限定名,如:java/lang/Object
父类索引(2字节)
该索引指向常量池中CONSTANT_Class_info结构。
接口计数器(2字节)
为类实现的接口数量
接口索引表
该索引集合每项指向常量池中CONSTANT_Class_info结构。
访问标识 21 即 ACC_PUBLIC , ACC_SUPER
类索引指向常量池中#5 字面值 = simple02/ByteCodeTest
父类索引指向常量池中#6 字面值 = java/lang/Object
接口计数器=0,所以后面没有接口索引表
字段计数器(2字节)
描述类中有多少字段。
字段索引集合
用于描述接口或类中声明的变量。字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量。
字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、 private 或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。
#字段结构
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
字段计数器 = 1 ,字段表项长度1,字段标识符 = 0x0002 即 ACC_PRIVATE, 字段名 索引=#7字面量= fieldInt,描述=#8 字面量I 即 基本数据类型int, attributes_count = 0
方法计数器(2字节)
表示该类中有多少个方法, 但不包括超类或超接口中的方法数量。
方法表
表中的每个成员都必须是一个method_info结构,用于表示当前类或接口中某个方法的完整描述。
如果某个method_info结构的access_flags项既没有设置 ACC_NATIVE 标志也没有设置ACC_ABSTRACT标志,那么该结构中也应包含实现这个方法所用的 Java虚拟机 指令。
method_info结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法和类或接口初始化方法·方法表的结构实际跟字段表是一样的:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
根据字节码,方法表中第一个方法有一个属性,属性指向的字面量是Code,其结构
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception _table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
code[]就是属于虚拟机的指令,它由一个字节长度的、代表着某种特定操作含义的数字(称为 操作码 ,Opcode)以及跟随其后的零至多个代表此操作所需参数(称为操作数,Operands)而构成。由于Java虚拟机采用面向操作数栈而不是 寄存器 的结构,所以大多数的指令都不包含操作数,只有一个操作码。
由于限制了 Java 虚拟机 操作码的长度为一个字节(即0~255),这意味着 指令集 的操作码总数不可能超过256条。
对照字节码指令索引,一个一个查字典方式去标识翻译命令,与javap的文件对比验证是否有误。
翻译后变成如下代码
public simple02. ByteCode Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field fieldInt:I
9: return
LineNumberTable:
line 3: 0
line 5: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lsimple02/ByteCodeTest;
终于到最后了。。。。。
attributes_count 、 attributes[]
属性表集合,指的是class文件所携带的辅助信息,比如该class文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解。
此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。
属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但]ava虚拟机运行时会忽略掉它不认识的属性。
名字索引得出属性是属于 SourceFile结构,定长占8个字节,指向常量池中#23,字面值为 ByteCodeTest.java
#属性结构
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
#SourceFile结构
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
总结
class文件的结构以每一项类似数组按顺序排列。[u4 magic][u2 minor_version][u2 major_version][…][…][…][…][…][…][…]
每项都有对应的结构,根据【java虚拟机规范】文档,好似查字典一样去解析即可。
文档目录