Java 类文件结构解析

本文是《深入理解Java虚拟机》中类文件结构一章的读书笔记。另外,推荐阅读Java字节码结构解析来加深理解。

Class文件组成内容

class文件是一组以8位字节为基础的二进制流,其与Java虚拟机指令集和符号表以及若干其他辅助信息相对应。

该设计有如下优点:

  1. 平台无关性,class文件可以运行在任意平台,无需考虑各个平台机器指令集不同的问题
  2. 语言无关性,不论何种语言,只要生成的class文件格式符合JVM虚拟机规范即可

注:如果遇到8位字节以上空间的数据,则会按照高位在前的方式分割成若干个8位字节进行存储(Big-Endian,具体是指最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据,它是SPARC、PowerPC等处理器的默认多字节顺序,而x86等处理器则是使用了相反的 Little-Endian 顺序来存储数据)

Class文件数据结构

class文件采用了类似C语言结构体)的形式来存储数据,主要有以下几个特点:

  • 由无符号数和表两种数据结构组成
  • 集合,用来描述同一类型但数量不定的多个数据,格式为 容量计数器 + 数据集合
  • 没有任何分割符号(每个字节代表的含义,长度,先后顺序都不允许改变)

无符号数

定义:class文件基本的数据类型,用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。

表现形式:以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数。

组成:由无符号数或者其他表作为数据项构成的复合数据类型

特征:以_info 结尾

功能:用于描述有层次关系复合结构的数据

整个class文件本质上就是一张表

Class文件数据项

按照class文件中字节码的顺序来介绍数据项。

魔数

class文件的头4个字节。

功能:验证该文件是否能够被虚拟机接受

扩展名可以被修改

主次版本号

魔数后4个字节,第5个和第6个字节是次版本号(Minor Version),第7个和第8个字节是主版本号(Major Version)。

Java版本号从45开始,每个大版本发布版本号 +1

虚拟机拒绝超过其版本号的Class文件

常量池

可以说常量池是class文件的资源仓库,主要存放两大类常量,字面量和符号引用。

结构:容量计数器(u2类型) + 常量

容量计数从1开始,目的是满足某些常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池”的含义。

字面量(Literal): 类似Java中的常量,如文本字符串,声明为final的常量值等。

符号引用(Symbolic References):包括类和接口的全限定名(Full Qualified Name),字段的名称和描述符(Descriptor),方法的名称和描述符这三类常量。

常量池中的表

每一项常量都是一个表。到JDK1.7为止,共有14种常量表类型,表结构见文章末尾(常量池中的14种常量项的结构总表)。

所有常量表开始第一位为u1类型的标志位,标识常量类型。

以下是常量池的项目类型表

类型标志描述
CONSTANT_Utf8_info1UTF-8编码的字符串
CONSTANT_Integer_info3整形字面量
CONSTANT_Float_info4浮点型字面量
CONSTANT_Long_info5长整形字面量
CONSTANT_Double_info6双精度浮点型字面量
CONSTANT_Class_info7类或接口的符号引用
CONSTANT_String_info8字符串类型字面量
CONSTANT_Fieldref_info9字段的符号引用
CONSTANT_Methodref_info10类中方法的符号引用
CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
CONSTANT_NameAndType_info12字段或方法的部分符号引用
CONSTANT_MethodHandle_info15表示方法句柄
CONSTANT_MethodType_info16表示方法类型
CONSTANT_InvokeDynamic_info18表示一个动态方法调用点

每一种常量类型有着自己的结构,下面以CONSTANT_Class_info类型为例,它的结构如下表:

类型名称数量
u1tag1
u2name_index1

上表中的tag用来区分常量类型,name_index是一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型常量,代表了这个类(或者接口)的权限定名。

访问标志

常量池之后两个字节标识类的访问标志,用于识别一些类或者接口层次的访问信息。

具体标志位及标志含义见下表

标志名称标志值含义
ACC_PUBLIC0x0001是否为public类型
ACC_FINAL0x0010是否被声明为final,只有类可设置
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语言,invokespecial指令的语意在JDK1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE0x0200标识这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
ACC_ANNOTATION0x2000标识这是一个注解
ACC_ENUM0x4000标识这是一个枚举

类索引、父类索引、接口索引

类索引、父类索引

类索引、父类索引都是一个u2类型的数据。它们会对应到常量池中的类描述符常量,通过常量中的索引值就可以找到类的全限定名字符串。

接口索引

接口索引集合是一组u2类型的数据的集合。第一项u2类型的数据为接口计数器,表示接口索引表的容量,如果该类没有实现任何接口,该计算器值为0。

Class文件中由这三项数据来确定类的继承关系。

字段表集合

字段表用于描述接口或者类中声明的变量,包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

字段表使用标志位表示修饰符,引用常量池中的常量描述字段名及字段数据类型。

字段表集合中不会列出从超类或者父接口中继承而来的字段,但可能列出原本Java代码之中不存在的字段,譬如,在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

Java语言中字段是无法重载的,必须使用不同的名称,但是对于字节码来说,字段可以重名,只要字段的描述符不一致

字段结构表

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

access_flags表示字段修饰符,与类的access_flags类似,并且都是一个u2的数据类型。

标志位及含义见下表

标志名称标志值含义
ACC_PUBLIC0x0001字段是否public
ACC_PRIVATE0x0002字段是否private
ACC_PROTECTED0x0004字段是否protected
ACC_STATIC0x0008字段是否static
ACC_FINAL0x0010字段是否final
ACC_VOLATILE0x0040字段是否volatile
ACC_TRANSIENT0x0080字段是否transient
ACC_SYNTHETIC0x1000字段是否由编译器自动产生的
ACC_ENUM0x4000字段是否enum

name_index和descriptor_index都是对常量池的引用。

name_index代表字段的简单名称。

descriptor_index代表字段和方法的描述符。

全限定名

将类全名中的“.”替换成“/”,并在最后添加一个“;”,表示全限定名结束

简单名称

没有类型和参数的方法或者字段名称

描述符

描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值

描述符规则

  • 基本数据类型(byte、char、double、float、int、long、short、boolean)以及void都用一个大写字符来表示

  • 对象类型用字符L加对象的全限定名来表示

  • 数组类型,每一纬度使用一个前置的“[”字符来描述,如定义为”java.lang.String[][]”,将被表示为”[[Ljava/lang/String”,一个整形数组”int[]”将被表示为”[I”

  • 描述方法时,参数列表在前,返回值在后,且参数列表需要按顺序放在一组小括号之内

方法表集合

Class文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式,只是在访问标志和属性表集合的可选项中有所区别。

方法结构见下表

类型名称数量
u2access_flags1
u2name_index1
u2descriptor_index1
u2attributes_count1
attribute_infoattributesattributes_count

方法表的访问标志中没有ACC_VOLATILE 和 ACC_TRANSIENT 标志,增加了 ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP、ACC_ABSTRACT 标志。

方法访问标志

标志名称标志值含义
ACC_PUBLIC0x0001方法是否为public
ACC_PRIVATE0x0002方法是否为private
ACC_PROTECTED0x0004方法是否为protected
ACC_STATIC0x0008方法是否为static
ACC_FINAL0x0010方法是否为final
ACC_SYNCHRONIZED0x0020方法是否为synchronized
ACC_BRIDGE0x0040方法是否由编译器产生的桥接方法
ACC_VARARGS0x0080方法是否接受不定参数
ACC_NATIVE0x0100方法是否为native
ACC_ABSTRACT0x0400方法是否为abstract
ACC_STRICTFP0x0800方法是否为strictfp
ACC_SYNTHETIC0x1000方法是否是由编译器自动产生的

如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息,但有可能出现由编译器自动添加的方法,如类构造器<clinit>方法和实例构造器<init>方法

属性表集合

Class文件、字段表、方法表都可以有自己的属性表集合,用于描述某些场景的专有信息。属性表集合的限制更宽松一些,不要求各个属性表具有严格顺序,并且只要不与已有属性名重复即可。

Code属性

用来存储Java程序方法体中的代码经过编译处理后生成的字节码指令。每个指令是一个u1类型的单字节,共可以表达256条指令。

类型名称数量含义
u2attribute_name_index1指向常量池中一个CONSTANT_Utf8_info类型的常量,来表示属性名称
u4attribute_length1属性值长度
u2max_stack1表示操作栈深度的最大值
u2max_locals1表示局部变量表所需的存储空间
u4code_length1表示代码字节码长度
u1codecode_length用来存储字节码指令的一系列字节流
u2exception_table_length1异常表长度
exception_infoexception_tableexception_table_lengthJava代码的一部分,用来实现Java异常及finally处理机制(而不是简单的跳转命令)
u2attributes_count1Code属性总数
attribute_infoattributesattributes_countCode属性

code_length类型为u4,理论上最大可以达到2^32-1,但虚拟机规定一个方法不能超过65535条字节码指令,否则Javac编译器会拒绝编译。

Slot是虚拟机为局部变量分配内存所只用的最小单位,Javac编译器会根据变量的作用域来分配Slot给各个变量使用。

Exception属性

用于列举方法中可能抛出的受查异常(throws 关键字后面列举的异常)。

LineNumberTable属性

用于描述Java源代码行号与字节码行号之间的对应关系。

可以在Javac中分别使用-g:none或-g:lines选项来取消或要求生成这项信息,如果选择不生成,在程序运行抛出异常时,堆栈中将不会显示出错的行号,在调试程序时也无法按照源码行设置断点

LocalVariableTable属性

用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。

可以在Javac中分别使用-g:none或-g:vars选项来取消或要求生成这项信息,如果没有生成该属性,对程序运行没有影响,只是对代码编写带来较大不便,而且在调试期间无法根据参数名称从上下文中获得参数值

SourceFile属性

该属性是一个定长属性,用于记录生成这个Class文件的源码文件名称。

类型名称数量
u2attribute_name_index1
u4attribute_length1
u2sourcefile_index1

可以使用Javac的-g:none或-g:source选项来关闭或要求生成这项信息,如果不生成这项属性,当抛出异常时堆栈中将不会显示出错误代码所属的文件名

ConstantValue属性

该属性为一个定长属性,用来通知虚拟机自动为静态变量赋值。

InnnerClass属性

用于记录内部类与宿主类之间的关联。

Deprecated及Synthetic属性

两者为标志类型的布尔属性,只存在是和否的区别,没有值的概念。

Deprecated属性表示某个类、字段或方法不在被推荐使用

Synthetic属性代表此字段或者方法并不是由Java源码直接产生的,而是由编译器自行添加的,比如Bridge Method

StackMapTable属性

Code属性中最多只能有一个StackMapTable属性,否则将抛出ClassFormatError异常

Signature属性

该属性是在JDK1.5发布后添加到Class文件规范的一个可选定长属性,可以出现在类、属性表和方法表的属性表中。

BootstrapMethods属性

该属性是在JDK1.7发布后增加到Class文件规范之中的一个复杂的变长属性,用于保存invokedynamic指令引用的引导方法限定符。

常量池中的14种常量项的结构总表

常量项目类型描述
CONSTANT_Utf8_infotagu1值为1
lengthu2UTF-8编码字符串占用的字节数
bytesu1长度为length的UTF-8编码的字符串
CONSTANT_Integer_infotagu1值为3
bytesu4按照高位在前存储的int值
CONSTANT_Float_infotagu1值为4
bytesu4按照高位在前存储的float值
CONSTANT_Long_infotagu1值为5
bytesu8按照高位在前存储的long值
CONSTANT_Double_infotagu1值为6
bytesu8按照高位在前存储的double值
CONSTANT_Class_infotagu1值为7
indexu2指向全限定名常量项的索引
CONSTANT_String_infotagu1值为8
indexu2指向字符串字面量的索引
CONSTANT_Fieldref_infotagu1值为9
indexu2指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
indexu2指向字段描述符CONSTANT_NameAndType的索引项
CONSTANT_Methodref_infotagu1值为10
indexu2指向声明方法的类描述符CONSTANT_Class_info的索引项
indexu2指向名称及类型描述符CONSTANT_NameAndType的索引项
CONSTANT_Interface_Methodref_infotagu1值为11
indexu2指向声明方法的接口描述符CONSTANT_Class_info的索引项
indexu2指向名称及类描述符CONSTANT_NameAndType的索引项
CONSTANT_NameAndType_infotagu1值为12
indexu2指向该字段或方法名称常量的索引
indexu2指向该字段或方法描述符常量项的索引
CONSTANT_MethodHandle_infotagu1值为15
reference_kindu1值必须在1-9之间(包括1和9),它决定了方法句柄的类型。方法句柄类型的值表示方法句柄的字节码行为
reference_indexu2值必须是对常量池的有效索引
CONSTANT_MethodType_infotagu1值为16
descriptor_indexu2值必须是对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info结构,表示方法的描述符
CONSTANT_InvokeDynamic_infotagu1值为18
bootstrap_method_attr_indexu2值必须是对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引
name_and_type_indexu2值必须是对当前常量池的有效索引,常量池在该索引出的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符

虚拟机规范预定义的属性

属性名称使用位置含义
Code方法表Java代码编译成的字节码指令
ConstantValue字段表final关键字定义的常量值
Deprecated类、方法表、字段表被声明为deprecated的方法和字段
Exceptions方法表方法抛出的异常
EnclosingMethod类文件仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
InnerClasses类文件内部类列表
LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
LocalVariableTableCode属性方法的局部变量描述
StackMapTableCode属性JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
Signature类、方法表、字段表JDK1.5中新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口,初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Type),则Signature 属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
SourceFile类文件记录源文件名称
SourceDebugExtension类文件JDK1.6中新增的属性,SourceDebugExtension属性用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension属性就可以用于存储这个标准所新加入的调试信息
Synthetic类、方法表、字段表标识方法或字段为编译器自动生成的
LocalvariableTypeTableJDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
RuntimeVisibleAnnotations类、方法表、字段表JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的
RuntimeInvisibleAnnotations类、方法表、字段表JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations属性作用刚好相反,用于指明那些注解是运行时不可见的
RuntimeVisibeParameterAnnotations方法表JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数
RuntimeInvisibeParameterAnnotations方法表JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations属性类似,只不过作用对象为方法参数
AnnotationDefault方法表JDK1.5中新增的属性,用于记录注解类元素的默认值
BootstrapMethods类文件JDK1.7中新增的属性,用于保存invokedynamic指令引用的引导方法限定符
    原文作者:JVM
    原文地址: https://juejin.im/entry/59145a30a22b9d0058f7626e
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞