【深入理解java虚拟机】 - class类文件结构
@[TOC]
概述
“一次编写,到处运行 (Write Once,Run Anywhere)”。
平台无关性
Oracle公司以及其他虚拟机发行商发布过许多可以运行在各 种不同硬件平台和操作系统上的Java虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节 码,从而实现了程序的“一次编写,到处运行”。
实现语言无关性的基础是虚拟机和字节码存储格式。==Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机 指令集、符号表以及若干其他辅助信息。==
无论使用何种语言进行软件开发,只要能将源文件编译为正确的Class文件,那么这种语言就可以在Java虚拟机上执行。当Java源代码成功编译成字节码后,在不同的平台上面运行,就无须再次编译。
Class类文件的结构
Java虚拟机规范—— class文件格式官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html
任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(如类或接口也可以动态生成,直接送入类加载器中)。
class文件格式
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 件之中,中间没有添加任何分隔符,所以在其中的数据项,无论是字节顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
Class文件格式采用一种类似于C语言结构体的伪结构来存储数 据,这种伪结构中只有两种数据类型:无符号数、表。
- 无符号数:无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串 值。
- 表:表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名 都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的 容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的“集合”。
class文件结构
首先打开一个.class文件看一看:
如何打开class文件不乱码?
当我们直接使用记事本或者notepad等打开一个class文件时会乱码,如:
> 我们可以使用以下方式正确打开class文件:
- 使用sublime编辑器直接打开
- 使用notepad++插件打开:
Class文件的结构并不是一成不变的,随着Java虚拟机的不断发展,总是不可避免地会对Class文件结构做出一些调整,但是其基本结构和框架是非常稳定的。它总体包括以下几个部分:
- 魔数(magic)
- class文件版本
- 常量池
- 访问标识
- 类索引、父类索引、接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
ClassFile {
u4 magic;
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];
}
魔数
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为 一个能被虚拟机接受的Class文件。
- 魔数固定为 oxcafebabe,不会改变
- 如果一个class文件不以oxcafebabe开头,虚拟机在进行文件校验的时候会直接抛出错误:
Error: A JNI error has occurred,please check your installation and try again
Exception in thread “main” java.lang.ClassFormatError:Incompatible magic value 1885430635 in classfile *** - 使用魔数也不是扩展名来识别字节码文件,是因为扩展名可以随意改动。
文件版本号
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。
代表次版本号的第5个和第6个字节值为0x0000,而主版本号的值 为0x0034,也即是十进制的52。
Java的版本号是从45开始的,JDK 1.1之后 的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能 向下兼容以前版本的Class文件,但不能运行以后版本的Class文件。
常量池
紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class 文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它 还是在Class文件中第一个出现的表类型数据项目。
这部分包含两块结构:常量池容量计数值、常量池表。
1.常量池容量计数值
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常 量池容量计数值。
这个容量计数是从1而不是0开始的,如上图的常量池容量为十六进制数0x001b,即十进制的27,这就 代表常量池中有26项常量,索引值范围为1~26。
2.常量池表
常量池中主要存放两大类常量:字面量和符号引用。
- 字面量:字面量比 较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
- 符号引用:符号引用则属于编译 原理方面的概念,主要包括下面几类常量:
被模块导出或者开放的包、类和接口的全限定名、字段的名称和描述符、方法的名称和描述符、方法句柄和方法类型、动态调用点和动态常量
常量池中每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了 更好地支持动态语言调用,额外增加了4种动态语言相关的常量,为了支持Java模块化系统 ,又加入了CONSTANT_Module_info和CONSTANT_Package_info两个常量,所以截至JDK 13,常量表中分别有17种不同类型的常量。
类型 | 标志 | 描述 |
---|---|---|
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_Dynamic_info | 17 | 表示一个动态计算常量 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Module_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 |
访问标识
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或 者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract 类型;如果是类的话,是否被声明为final;等等。
具体的标志位以及标志的含义如下表所示:
类型 | 标志 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语义 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类型值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的标识 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
ACC_MODULE | 0x8000 | 标识这是一个模块 |
类索引、父类索引与接口索引集合
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,Class文件中由这三项数据来确定该类型的继承关系。
- 类索引用于确定这个类的全限定名;
- 父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多 重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。
- 接口索引集合就用来描述这个类实现了哪些接 口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是 extends关键字)后的接口顺序从左到右排列在接口索引集合中。
类索引和父类索引都是一个u2类型的数据,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过 CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的 全限定名字符串。
而接口索引集合 是一组u2类型的数据的集合,索引集合入口的第一项u2类型的数据为接口计数器,表示索引表 的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
字段表集合
字段表用于描述接口或者类中声明的变量。
- 字段(field)包括类级变量以及实例级变量,但是不包括方法内部、代码块内部声明的局部变量。
- 字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。
- 它指向常量池索引集合,它描述了每个字段的完整信息。比如字段的标识符、访问修饰符(public、private或protected)、是类变量还是实例变量(static修饰符)、是否是常量(final修饰符)等。
字段计数器的值表示当前class文件fields表的成员个数。使用两个字节来表示。
字段表中每个成员都是一个field_info结构,用于表示该类或接口所声明的所有类字段或者实例字段,不包括方法内部声明的变量,也不包括从父类或父接口继承的那些字段。
字段表作为一个表,也有自己的结构:
类型 | 名称 | 含义 | 数量 |
---|---|---|---|
u2 | access_flags | 访问标志 | 1 |
u2 | name_index | 字段名索引 | 1 |
u2 | descriptor_index | 描述符索引 | 1 |
u2 | attributes_count | 属性计数器 | 1 |
attribute_info | attributes | 属性集合 | attributes_count |
- 访问标识
标志名称 | 标志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_PRIVATE | 0x0002 | 字段是否private |
ACC_PROTECTED | 0x0004 | 字段是否protected |
ACC_STATIC | 0x0008 | 字段是否static |
ACC_FINAL | 0x0010 | 字段是否final |
ACC_VOLATILE | 0x0040 | 字段是否volatile |
ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动生成 |
ACC_ENUM | 0x4000 | 字段是否enum |
-
字段名索引
根据字段名索引的值,查询常量池中的指定索引项。 -
描述符索引
描述符的作用是用来描述字段 的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类 型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大 写字符来表示,而对象类型则用字符L加对象的全限定名来表示。
-
属性表集合
属性表集合也包括属性计数器和属性集合。用于存储一些额外的信息,字段表可以在属性表中附加描述零至多项的额外信息。
字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不存在的字段,如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段。
另外,字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使 用不一样的名称,但是对于Class文件格式来讲,只要两个字段的描述符不是完全相同,那字段重名就 是合法的。
方法表集合
Class文件存储 格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依 次包括访问标志、名称索引、描述符索引、属性表集合几项。
方法表集合的访问标志如下:
标志名称 | 标志值 | 描述 |
---|---|---|
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_BRIDGE | 0x0040 | 方法是不是由编译器产生的桥接方法 |
ACC_VARAGES | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为本地方法 |
ACC_ABSTRACT | 0x0400 | 方法是否public |
ACC_STRICT | 0x0800 | 方法是否private |
ACC_SYNTHETIC | 0x1000 | 方法是否protected |
属性表集合
方法表集合之后的属性表集合,指的是class文件所携带的辅助信息,比如该class 文件的源文件的名称。以及任何带有RetentionPolicy.CLASS 或者RetentionPolicy.RUNTIME的注解。这类信息通常被用于Java虚拟机的验证和运行,以及Java程序的调试,一般无须深入了解。
此外,字段表、方法表都可以有自己的属性表。用于描述某些场景专有的信息。
属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但Java虚拟机运行时会忽略掉它不认识的属性。
属性表集合的属性非常多,具体可以查看官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7
- 点赞
- 收藏
- 关注作者
评论(0)