查看原文
其他

走进字节码--class文件结构

GeekJunz 极客创享会 2019-07-14


一、无关性

Java诞生之初,就提出了著名的口号“一次编写,到处运行(Write Once, Run Anywhere)”。解决了不同平台、系统间的兼容问题,极大的方便了开发人员,也因此成为最受欢迎的编程语言。时至今日,依然保持着活力。当下语言的蓬勃发展,很多语言如Python、Ruby、Perl等凭借着强大的解释器生来就具备平台无关性。跨平台成为时下编程语言的一种趋势。
但Java的无关性不仅仅于此,更强大之处在于其语言无关性,不管是java,还是python、groovy等都可以在java虚拟机上运行(Java虚拟机设计伊始目标就是跨语言的,Java虚拟机规范和Java语言规范也是相互独立的)。而这一切的基础,就是强大的class 文件结构,是各类语言与Java虚拟机之间的重要桥梁。

注意:
1、Java 语言是平台无关的,但是虚拟机是平台相关的。(需使用平台底层命令实现)
2、各类语言编译为统一格式的 .class 文件,需要对应各类语言的编译器来实现。

以下为无关性示意图:

无关性原理

二、class文件结构

1、补充

每一个 Class 文件都对应着唯一一个类或接口的定义信息
每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、 32 位和 64 位长度的数据将被构造成 2 个、 4 个和 8 个 8 字节单位来表示。多字节数据项总是按照 Big-Endian①的顺序进行存储。

注1
Big-Endian 顺序是指按高位字节在地址最低位,最低字节在地址最高位来存储数据;
Little-Endian 与之相反。
注2
定义了一组私有数据类型来表示 Class 文件的内容,它们包括 u1, u2 和 u4,分别代表了 1、 2 和 4 个字节的无符号数。

2、class 文件结构如下:

1ClassFile {
2    u4 magic;
3    u2 minor_version;
4    u2 major_version;
5    u2 constant_pool_count;
6    cp_info constant_pool[constant_pool_count-1];
7    u2 access_flags;
8    u2 this_class;
9    u2 super_class;
10    u2 interfaces_count;
11    u2 interfaces[interfaces_count];
12    u2 fields_count;
13    field_info fields[fields_count];
14    u2 methods_count;
15    method_info methods[methods_count];
16    u2 attributes_count;
17    attribute_info attributes[attributes_count];
18}

class 文件中的字节码是严格按照这个顺序存储的。且没有分隔符。下一节实战分析就能够充分体会到。

其中
magic: 魔数,魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件。 魔数值固定为 0xCAFEBABE, 不会改变(该词寓意当时很火的一款咖啡)。
minor_versionmajor_version: 副版本号和主版本号, minor_version 和 major_version 的值分别表示 Class 文件的副、 主版本。 它们共同构成了 Class 文件的格式版本号。只有版本在支持的范围之内才能够被虚拟机识别,同时虚拟机时向下兼容的。

constant_pool_count:常量池项个数,constant_pool_count 的值等于 constant_pool 表中的成员数加 1。constant_pool 表的索引值只有在大于 0 且小于 constant_pool_count 时才会被认为是有效的。

constant_pool[]: 它包含 Class 文件结构及其子结构中引用的所有字符串常量、 类或接口名、字段名和其它常量。

1、常量池中的每一项都具备相同的格式特征——第一个字节作为类型标记用于识别该项是哪种类型的常量,称为“tagbyte”。
2、常量池的索引范围是 1 至 constant_pool_count−1。常量池第0项保留:
目的在于满足以后某些指向常量池的索引值的数据在特定情况下需要表达‘不指向任何一个常量池项目’的含义,这时可以用索引值置为 0 表示。
3、常量池主要存放两类常量:字面量和符号引用
字面量: 和Java 中的常量类似,如文本字符串、声明为 final 的常量值等。
符号引用: 包含如下三类常量

  • 类和接口的全限定名(Fully Qualified Name)

  • 字段的名称和描述符

  • 方法的名称和描述符

常量池通用格式如下

1cp_info {
2    u1 tag;
3    u1 info[];
4}

tag 表示常量类型,简要整理如下(具体类型细节后续整理贴出):

常量类型描述
CONSTANT_Class_info7表示类或接口
CONSTANT_Fieldref_info9字段信息表
CONSTANT_Methodref_info10方法
CONSTANT_InterfaceMethodref_info11接口方法
CONSTANT_String_info8java.lang.String 类型的常量对象
CONSTANT_Integer_info3整型字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整型字面量
CONSTANT_Double_info6双精度型字面量
CONSTANT_NameAndType_info12名称和类型表
CONSTANT_Utf8_info1utf-8 编码的字符串
CONSTANT_MethodHandle_info15方法句柄表
CONSTANT_MethodType_info16方法类型表
CONSTANT_InvokeDynamic_info18动态方法调用点

具体每种类型的结构请参考:

走进字节码专题--常量池 附录

access_flags: 访问标志, access_flags 是一种掩码标志, 用于表示某个类或者接口的访问权限及基础属性。 access_flags 的取值范围和相应含义见下表所示。

标记名含义
ACC_PUBLIC0x0001可以被包的类外访问。
ACC_FINAL0x0010不允许有子类。
ACC_SUPER0x0020"当用到 invokespecial指令时,需要特殊处理③的父类方法。"
ACC_INTERFACE0x0200标识定义的是接口而不是类。
ACC_ABSTRACT0x0400不能被实例化。
ACC_SYNTHETIC0x1000标识并非 Java 源码生成的代码。
ACC_ANNOTATION0x2000标识注解类型
ACC_ENUM0x4000标识枚举类型

this_class: 类索引, this_class 的值必须是对 constant_pool 表中项目的一个有效索引值。constant_pool 表在这个索引处的项必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口。

super_class: 父类索引,对于类来说, super_class 的值必须为 0 或者是对 constant_pool 表中项目的一个有效索引值。

  • 如果它的值不为 0,那 constant_pool 表在这个索引处的项必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类的直接父类。 当前类的直接父类,以及它所有间接父类的 access_flag 中都不能带有 ACC_FINAL 标记。

  • 对于接口来说, 它的 Class 文件的 super_class 项的值必须是对 constant_pool 表中项目的一个有效索引值。

  • constant_pool 表在这个索引处的项必须为代表 java.lang.Object 的 CONSTANT_Class_info 类型常量。

  • 如果 Class 文件的 super_class 的值为 0,那这个 Class 文件只可能是定义的是java.lang.Object 类,只有它是唯一没有父类的类。

interfaces_count: 表示当前类或接口的直接父接口数量。

interfaces[]: interfaces[]数组中的每个成员的值必须是一个对 constant_pool 表中项目的一个有效索引值, 它的长度为 interfaces_count。每个成员 interfaces[i] 必须为 CONSTANT_Class_info 类型常量,其中 0 ≤ i < interfaces_count。

fields_count: fields_count 的值表示当前 Class 文件 fields[]数组的成员个数。fields[]数组中每一项都是一个 field_info 结构的数据项, 它用于表示该类或接口声明的类字段或者实例字段。

fields[]:  fields[]数组中的每个成员都必须是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述。 fields[]数组描述当前类或接口声明的所有字段,但不包括从父类或父接口继承的部分。

1//字段通用结构
2field_info {
3    u2 access_flags;
4    u2 name_index;
5    u2 descriptor_index;
6    u2 attributes_count;
7    attribute_info attributes[attributes_count];
8}

【具体字段结构特点参考后续附录】

methods_count: methods_count 的值表示当前 Class 文件 methods[]数组的成员个数。Methods[]数组中每一项都是一个 method_info 结构的数据项.

methods[]: methods[]数组中的每个成员都必须是一个 method_info 结构的数据项,用于表示当前类或接口中某个方法的完整描述。

  • method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法方法和类或接口初始化方法方法。

  • methods[]数组只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。

1//方法表通用结构
2method_info {
3    u2 access_flags;
4    u2 name_index;
5    u2 descriptor_index;
6    u2 attributes_count;
7    attribute_info attributes[attributes_count];
8}

【具体方法结构特点参考后续附录】

attributes_count: attributes_count 的值表示当前 Class 文件 attributes表的成员个数。 attributes 表中每一项都是一个 attribute_info 结构的数据项。

attributes[]: attributes 表的每个项的值必须是 attribute_info 结构.

1//属性表通用结构
2attribute_info {
3    u2 attribute_name_index;
4    u4 attribute_length;
5    u1 info[attribute_length];
6}

【具体各类属性结构特点参考后续附录】

三、描述符

1、全限定名 和 非全限定名
全限定名

  • 在 Class 文件结构中出现的类或接口的名称, 都通过全限定形式(Fully Qualified Form)来表示, 这被称作它们的“二进制名称”。

  • 这个名称使用 CONSTANT_Utf8_info结构来表示

  • 譬如,类 Thread 的正常的二进制名是 java.lang.Thread。在 Class 文件的内部表示形式里面,对类 java.lang.Thrad 的引用是通过来一个代表字符串“java/lang/Thread” 的CONSTANT_Utf8_info 结构来实现的。

非全限定名

  • 方法名,字段名和局部变量名都被使用非全限定名(Unqualified Names)进行存储。

  • 非全限定名中不能包含 ASCII 字符"."、 ";"、 "["和"/"(也不能包含他们的Unitcode 表示形式,既类似"\u2E"这种形式)。

  • 方法的非全限定名还有一些额外的限制,除了实例初始化方法“

    ”和类初始化方法“”以外,其他方法非全限定名中不能包含 ASCII 字符"<"和">"(也不能包含他们的Unicode 表示形式)

2、字段描述符 和 方法描述符
字段描述符(Field Descriptor):  是一个表示类、 实例或局部变量的语法符号,它是由语法产生的字符序列:

1//基本结构如下
2FieldDescriptor:
3    FieldType
4ComponentType:
5    FieldType
6FieldType:
7    BaseType
8    ObjectType
9    ArrayType
10BaseType:
11    B 
12    C
13    D
14    F
15    I
16    J
17    S
18    Z
19ObjectType:
20    L Classname ;
21ArrayType:
22    [ ComponentType
  • 所有表示基本类型(BaseType) 的字符、表示对象类型(ObjectType) 中的字符"L", 表示数组类型(ArrayType) 的"["字符都是 ASCII 编码的字符。

  • 对象类型(ObjectType) 中的 Classname 表示一个类或接口二进制名称的内部格式。

  • 表示数组类型的有效描述符的长度应小于等于 255。

所有字符类型的解释整理如下:

字符类型含义
Bbyte有符号字节型数
CcharUnicode 字符, UTF-16 编码
Ddouble双精度浮点数
Ffloat单精度浮点数
Iint整型数
Jlong长整数
Sshort有符号短整数
Zboolean布尔值 true/false
L Classname;reference一个名为的实例
[reference一个一维数组

举几个例子:

  • 描述 int 实例变量的描述符是“I

  • java.lang.Object 的实例描述符是 “Ljava/lang/Object;

  • double 的三维数组“double d[][][];” 的描述符为“[[[D

方法描述符(Method Descriptor): 描述一个方法所需的参数和返回值信息:

1MethodDescriptor:
2( ParameterDescriptor* ) ReturnDescriptor
3
4ParameterDescriptor: //参数描述符描述需要传给这个方法的参数信息
5    FieldType
6ReturnDescriptor:    //返回值描述符从当前方法返回的值
7    FieldType
8    VoidDescriptor
9VoidDescriptor:
10    V

举个例子
方法 : Object mymethod(int i, double d, Thread t)
描述符: (IDLjava/lang/Thread;)Ljava/lang/Object;

注:参数放在括号里面,有几个参数写几个,对象类型为L跟着具体的对象类型,括号外为返回值描述符。此处为对象类型,所以为L 跟着具体的对象类型。

好了,以上就是Class 文件结构的主要内容,下一小结描述一个简单的class 文件的完整分析。


END

喜欢随手转发点赞^_^


极客创享会
干货分享、结识更多极客


    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存