java class文件详解
一寸HUI 人气:0一、概述
各种不同平台的Java虚拟机, 以及所有平台都统一支持的程序存储格式——字节码(Byte Code)是构成平台无关性的基石,所以class文件主要用于解决平台无关性的中间文件。如下图所示:
java虚拟机不与包括Java语言在内的任何程序语言绑定, 它只与“Class文件”这种特定的二进制文件格式所关联, Class文件中包含了Java虚拟机指令集、 符号表以及若干其他辅助信息。
每一个class文件都对应着唯一一个类或者接口的定义信息,但是相对地,类或者接口并不一定都必须定义在文件里(比如类或者接口也可以通过类加载器直接生成)
每个class文件都是由字节流组成,各个数据项目严格按照顺序紧凑地排列在文件之中, 中间没有添加任何分隔符,每个字节流含有8个二进制位,所有的16位,32位和64位长度的数据将通过2个,4个和8个连续的8位字节来对其进行表示,多字节数据总是按照big-endian(大端在前:也就是说高位字节存储在低的地址上面,而低位字节存储到高地址上面)的顺序进行存储,在Java JDK中,可以使用java.io.DataInput、java.io.DataOutput等接口和java.io.DataInputStream和java.io.DataOutputStream等类来访问这种格式的数据Class文件结构采用类似C语言的结构体来存储数据的。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,主要有两类数据项,无符号数和表,无符号数用来表述数字,索引引用以及字符串等,比如 u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节的无符号数,而表是任意数量的可变长项组成,是有多个无符号数以及其它的表组成的复合结构,所有表的命名都习惯性地以“_info”结尾,无论是无符号数还是表, 当需要描述同一类型但数量不定的多个数据时, 经常会使用一个前置的容量计数器加若干个连续的数据项的形式, 这时候称这一系列连续的某一类型的数据为某一类型的“集合”。
二、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.1、魔数和java版本号
每个Class文件的头4个字节被称为魔数(Magic Number) , 它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。Class文件的魔数取得很有“浪漫气息”,
值为0xCAFEBABE(咖啡宝贝? )
紧接着魔数的4个字节存储的是Class文件的版本号: 第5和第6个字节是次版本号(MinorVersion) , 第7和第8个字节是主版本号(Major Version)
这里我们使用一个简单的代码进行分析:
public class TestClass { private int m; public int inc() { return m + 1; } }
使用javac命令对其进行编译,并使用WinHex (下载地址:http://www.x-ways.net/winhex/index-m.html)工具打开,得到如下的图,前面几位就是魔数和版本号
这里可以得出我们使用的版本为java1.8,16进制的34等于10进制的52
2.2、常量池
紧接着主、 次版本号之后的是常量池入口, 常量池可以比喻为Class文件里的资源仓库, 它是Class文件结构中与其他项目关联最多的数据, 通常也是占用Class文件空间最大的数据项目之一, 另外, 它还是在Class文件中第一个出现的表类型数据项目 ,常量池的入口需要放置一项u2类型的数据, 代表常量池容量计数值(constant_pool_count) ,这个容量计数是从1开始的。如下图所示:常量池容量(偏移地址: 0x00000008) 为十六进制数0x0013,则十进制为19,则这里有18个长常量,索引范围为1-18,在Class文件格式规范制定之时, 设计者将第0项常量空出来是有特殊考虑的, 这样做的目的在于, 如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义, 可以把索引值设置为0来表示。
然后我们使用javap命令查看该class文件:(这里明显显示为18个常量)
常量池中主要存放两大类常量: 字面量(Literal) 和符号引用(Symbolic References) 。
字面量比较接近于Java语言层面的常量概念, 如文本字符串、 被声明为final的常量值等。
符号引用则属于编译原理方面的概念, 主要包括下面几类常量:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、 Method Type、 Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、 Dynamically-Computed Constant)
虚拟机在加载Class文件时才会进行动态连接,也就是说,Class文件中不会保存各个方法、 字段最终在内存中的布局信息, 这些字段、 方法的符号引用不经过虚拟机在运行期转换的话是无法得到真正的内存入口地址, 也就无法直接被虚拟机使用的,当虚拟机做类加载时, 将会从常量池获得对应的符号引用, 再在类创建时或运行时解析、 翻译到具体的内存地址之中常量池中每一项常量都是一个表,截至JDK13, 常量表中分别有17种不同类型的常量。这17类表都有一个共同的特点, 表结构起始的第一位是个u1类型的标志位,代表着当前常量属于哪种常量类型。 17种常量类型所代表的具体含义如下图所示。
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了
类型 |
项目 |
类型 |
描述 |
CONSTANT_Utf8_info |
tag |
u1 |
值为1 |
length |
u2 |
utf-8缩略编码字符串占用字节数 |
|
bytes |
u1 |
长度为length的utf-8缩略编码字符串 |
|
CONSTANT_Integer_info |
tag |
u1 |
值为3 |
bytes |
u4 |
按照高位在前储存的int值 |
|
CONSTANT_Float_info |
tag |
u1 |
值为4 |
bytes |
u4 |
按照高位在前储存的float值 |
|
CONSTANT_Long_info |
tag |
u1 |
值为5 |
bytes |
u8 |
按照高位在前储存的long值 |
|
CONSTANT_Double_info |
tag |
u1 |
值为6 |
bytes |
u8 |
按照高位在前储存的double值 |
|
CONSTANT_Class_info |
tag |
u1 |
值为7 |
index |
u2 |
指向全限定名常量项的索引 |
|
CONSTANT_String_info |
tag |
u1 |
值为8 |
index |
u2 |
指向字符串字面量的索引 |
|
CONSTANT_Fieldref_info |
tag |
u1 |
值为9 |
index |
u2 |
指向声明字段的类或接口描述符CONSTANT_Class_info的索引项 |
|
index |
u2 |
指向字段描述符CONSTANT_NameAndType_info的索引项 |
|
CONSTANT_Methodref_info |
tag |
u1 |
值为10 |
index |
u2 |
指向声明方法的类描述符CONSTANT_Class_info的索引项 |
|
index |
u2 |
指向名称及类型描述符CONSTANT_NameAndType_info的索引项 |
|
CONSTANT_InterfaceMethodref_info |
tag |
u1 |
值为11 |
index |
u2 |
指向声明方法的接口描述符CONSTANT_Class_info的索引项 |
|
index |
u2 |
指向名称及类型描述符CONSTANT_NameAndType_info的索引项 |
|
CONSTANT_NameAndType_info
|
tag |
u1 |
值为12 |
index |
u2 |
指向该字段或方法名称常量项的索引 |
|
index |
u2 |
指向该字段或方法描述符常量项的索引 |
|
CONSTANT_MethodHandle_info |
tag |
u1 |
值为15 |
refrence_kind | u1 | 值必须在1-9之间,决定了方法句柄的类型,方法句柄的类型的值表示方法句柄字节码的行为 | |
refrence_index | u2 | 值必须是对常量池的有效索引 | |
CONSTANT_MethodType_info | tag | u1 | 值为16 |
descriptor_index | u2 | 值必须对常量池的有效索引,常量池在该处的项必须是CONSTANT_Utf8_info表示方法的描述符 | |
CONSTANT_Dynamic_info | tab | u1 | 值为17 |
bootstrap_method_attr_index | u2 | 值必须对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 | |
name_and_type_index | u2 | 值必须对当前常量池的有效索引,常量池中在该索引出的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 | |
CONSTANT_InvokeDynamic_info | tag | u1 | 值为18 |
bootstrap_method_attr_index | u2 | 值必须对当前Class文件中引导方法表的bootstrap_methods[]数组的有效索引 | |
name_and_type_index | u2 | 值必须对当前常量池的有效索引,常量池中在该索引出的项必须是CONSTANT_NameAndType_info结构,表示方法名和方法描述符 | |
CONSTANT_Module_info | tag | u1 | 值为19 |
name_index | u2 | 值必须对常量池的有效索引,常量池在该处的项必须是CONSTANT_Utf8_info表示模块名 | |
CONSTANT_Package_info | tag | u1 | 值为20 |
name_index | u2 | 值必须对常量池的有效索引,常量池在该处的项必须是CONSTANT_Utf8_info表示包名 |
2.3、访问标志
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型,如果是类的话,是否被声明为final等,具体的标志位以及标志的含义如下:
字段的访问权限 |
||
Flag Name |
Value |
Remarks |
ACC_PUBLIC |
0x0001 |
pubilc,包外可访问。 |
ACC_PRIVATE |
0x0002 |
private,只可在类内访问。 |
ACC_PROTECTED |
0x0004 |
protected,类内和子类中可访问。 |
ACC_STATIC |
0x0008 |
static,静态。 |
ACC_FINAL |
0x0010 |
final,常量。 |
ACC_VOILATIE |
0x0040 |
volatile,直接读写内存,不可被缓存。不可和ACC_FINAL一起使用。 |
ACC_TRANSIENT |
0x0080 |
transient,在序列化中被忽略的字段。 |
ACC_SYNTHETIC |
0x1000 |
synthetic,由编译器产生,不存在于源代码中。 |
ACC_ENUM |
0x4000 |
enum,枚举类型字段 |
ACC_MODULE |
0x8000 |
标识这是一个模块 |
2.4、类索引、 父类索引与接口索引集合
类索引(this_class) 和父类索引(super_class) 都是一个u2类型的数据, 而接口索引集合(interfaces) 是一组u2类型的数据的集合, Class文件中由这三项数据来确定该类型的继承关系。 类索引用于确定这个类的全限定名, 父类索引用于确定这个类的父类的全限定名。 由于Java语言不允许多重继承, 所以父类索引只有一个, 除了java.lang.Object之外, 所有的Java类都有父类, 因此除了java.lang.Object外, 所有Java类的父类索引都不为0。 接口索引集合就用来描述这个类实现了哪些接口, 这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口, 则应当是extends关键字) 后的接口顺序从左到右排列在接口索引集合中。
2.5、字段表集合
字段表(field_info) 用于描述接口或者类中声明的变量。 Java语言中的“字段”(Field) 包括类级变量以及实例级变量, 但不包括在方法内部声明的局部变量。 字段可以包括的修饰符有字段的作用域(public、 private、 protected修饰符) 、 是实例变量还是类变量(static修饰符) 、 可变性(final) 、 并发可见性(volatile修饰符, 是否强制从主内存读写) 、 可否被序列化(transient修饰符) 、 字段数据类型(基本类型、 对象、 数组) 、字段名称。 上述这些信息中, 各个修饰符都是布尔值, 要么有某个修饰符, 要么没有, 很适合使用标志位来表示。 而字段叫做什么名字、 字段被定义为什么数据类型, 这些都是无法固定的, 只能引用常量池中的常量来描述。 字段表的最终格式如下。
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
而字段修饰符放在access_flags项目中, 它与类中的access_flags项目是非常类似的, 都是一个u2的数据类型, 其中可以设置的标志位和含义如下所示:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否为public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_SYNCHRONIZED | 0x0020 | 字段是否为synchronized |
ACC_TRANSIENT | 0x0080 | 字段是否为transient |
ACC_ABSTRACT | 0x0400 | 字段是否为abstract |
ACC_SYNTHETIC | 0x1000 | 字段是否为编译器自动产生 |
name_index和descriptor_index。 它们都是对常量池项的引用, 分别代表着字段的简单名称以及字段和方法的描述符。
全限定名:仅仅是把类全名中的“.”替换成了“/”而已,例如类名org.apache.xxxx,器全限定名为org/apache/xxxx。
简单名称:就是指没有类型和参数修饰的方法或者字段名称, 比如类中的inc()方法和m字段的简单名称分别就是“inc”和“m”。
方法和字段的描述符:描述符的作用是用来描述字段的数据类型、 方法的参数列表(包括数量、 类型以及顺序) 和返回值。 根据描述符规则, 基本数据类型(byte、 char、 double、 float、 int、 long、 short、 boolean) 以及代表无返回值的void类型都用一个大写字符来表示, 而对象类型则用字符L加对象的全限定名来表示,祥见下表:
标识字符 | 含义 |
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,如java/lang/Object |
对于数组类型, 每一维度将使用一个前置的“[”字符来描述, 如一个定义为“java.lang.String[][]”类型的二维数组将被记录成“[[Ljava/lang/String; ”, 一个整型数组“int[]”将被记录成“[I”
用描述符来描述方法时, 按照先参数列表、 后返回值的顺序描述, 参数列表按照参数的严格顺序放在一组小括号“()”之内。 如方法void inc()的描述符为“()V”, 方法java.lang.String toString()的描述符为“()Ljava/lang/String; ”, 方法int indexOf(char[]source, int sourceOffset, int sourceCount, char[]target,int targetOffset, int targetCount, int fromIndex)的描述符为“([CII[CIII)I”
2.6、方法表集合
Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式, 方法表的结构如同字段表一样, 依次包括访问标志(access_flags) 、 名称索引(name_index) 、 描述符索引(descriptor_index) 、 属性表集合(attributes) 几项,如下图所示
在访问标志和属性表集合的可选项中有所区别,因为volatile关键字和transient关键字不能修饰方法, 所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。 与之相对, synchronized、 native、 strictfp和abstract关键字可以修饰方法, 方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、ACC_NATIVE、 ACC_STRICTFP和ACC_ABSTRACT标志。
2.7、属性表集合
1、code属性
方法的定义可以通过访问标志、 名称索引、 描述符索引来表达清楚, 但方法里面的代码去哪里了? 方法里的Java代码, 经过Javac编译器编译成字节码指令之后, 存放在方法属性表集合中一个名为“Code”的属性里面, 属性表作为Class文件格式中最具扩展性的一种数据项目,
java程序方法体里面的代码经过Javac编译器处理之后, 最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中, 但并非所有的方法表都必须存在这个属性, 譬如接口或者抽象类中的方法就不存在Code属性。
Code属性是Class文件中最重要的一个属性, 如果把一个Java程序中的信息分为代码(Code, 方法体里面的Java代码) 和元数据(Metadata, 包括类、 字段、 方法定义及其他信息) 两部分, 那么在整个Class文件里, Code属性用于描述代码, 所有的其他数据项目都用于描述元数据。
2、Exceptions属性
Exceptions属性的作用是列举出方法中可能抛出的受查异常(Checked Excepitons) , 也就是方法描述时在throws关键字后面列举的异常。
3、LineNumberTable属性
LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量) 之间的对应关系。并不是运行时必需的属性, 但默认会生成到Class文件之中, 可以在Javac中使用-g: none或-g: lines选项来取消或要求生成这项信息。
4、LocalVariableTable及LocalVariableTypeTable属性
LocalVariableTable属性用于描述栈帧中局部变量表的变量与Java源码中定义的变量之间的关系, 它也不是运行时必需的属性, 但默认会生成到Class文件之中, 可以在Javac中使用-g: none或-g: vars选项来取消或要求生成这项信息
5、SourceFile及SourceDebugExtension属性
SourceFile属性用于记录生成这个Class文件的源码文件名称。 这个属性也是可选的, 可以使用Javac的-g: none或-g: source选项来关闭或要求生成这项信息。 在Java中, 对于大多数的类来说, 类名和文件名是一致的, 但是有一些特殊情况(如内部类) 例外
SourceDebugExtension属性用于存储额外的代码调试信息。 典型的场景是在进行JSP文件调试时, 无法通过Java堆栈来定位到JSP文件的行号。
6、ConstantValue属性
ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。 只有被static关键字修饰的变量(类变量) 才可以使用这项属性。 类似“int x=123”和“static int x=123”这样的变量定义在Java程序里面是非常常见的事情, 但虚拟机对这两种变量赋值的方式和时刻都有所不同。 对非static类型的变量(也就是实例变量) 的赋值是在实例构造器<init>()方法中进行的; 而对于类变量, 则有两种方式可以选择: 在类构造器<clinit>()方法中或者使用ConstantValue属性。
7、InnerClasses属性
InnerClasses属性用于记录内部类与宿主类之间的关联。 如果一个类中定义了内部类, 那编译器将会为它以及它所包含的内部类生成InnerClasses属性
8、Deprecated及Synthetic属性
Deprecated和Synthetic两个属性都属于标志类型的布尔属性, 只存在有和没有的区别, 没有属性值的概念。
Deprecated属性用于表示某个类、 字段或者方法, 已经被程序作者定为不再推荐使用, 它可以通过代码中使用“@deprecated”注解进行设置
Synthetic属性代表此字段或者方法并不是由Java源码直接产生的, 而是由编译器自行添加的, 在JDK 5之后, 标识一个类、 字段或者方法是编译器自动产生的, 也可以设置它们访问标志中的ACC_SYNTHETIC标志位。
9、StackMapTable属性
StackMapTable是一个相当复杂的变长属性, 位于Code属性的属性表中。 这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证器(TypeChecker), 目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器。
StackMapTable属性中包含零至多个栈映射帧(Stack Map Frame) , 每个栈映射帧都显式或隐式地代表了一个字节码偏移量, 用于表示执行到该字节码时局部变量表和操作数栈的验证类型。 类型检查验证器会通过检查目标方法的局部变量和操作数栈所需要的类型来确定一段字节码指令是否符合逻辑约束。
10、Signature属性
Signature属性是一个可选的定长属性, 可以出现于类、 字段表和方法表结构的属性表中。 任何类、 接口、 初始化方法或成员的泛型签名如果包含了类型变量(Type Variable) 或参数化类型(ParameterizedType) , 则Signature属性会为它记录泛型签名信息。 之所以要专门使用这样一个属性去记录泛型类型, 是因为Java语言的泛型采用的是擦除法实现的伪泛型, 字节码(Code属性) 中所有的泛型信息编译(类型变量、 参数化类型) 在编译之后都通通被擦除掉。
11、BootstrapMethods属性
BootstrapMethods是一个复杂的变长属性, 位于类文件的属性表中。 这个属性用于保存invokedynamic指令引用的引导方法限定符。
12、MethodParameters属性
MethodParameters是一个用在方法表中的变长属性。MethodParameters的作用是记录方法的各个形参名称和信息。
13、模块化相关属性
JDK 9的一个重量级功能是Java的模块化功能, 因为模块描述文件(module-info.java) 最终是要编译成一个独立的Class文件来存储的, 所以, Class文件格式也扩展了Module、 ModulePackages和ModuleMainClass三个属性用于支持Java模块化相关功能。
Module属性是一个非常复杂的变长属性, 除了表示该模块的名称、 版本、 标志信息以外, 还存储了这个模块requires、 exports、 opens、 uses和provides定义的全部内容,
ModulePackages是另一个用于支持Java模块化的变长属性, 它用于描述该模块中所有的包, 不论是不是被export或者open的。
ModuleMainClass属性是一个定长属性, 用于确定该模块的主类(Main Class)
参考:
《深入理解java虚拟机第三版》
加载全部内容