Java虚拟机和Class文件是Java实现系统无关性的基石。
Class文件是JVM实现语言无关性的基石。
Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。
每一个 Class 文件对应于一个如下所示的 ClassFile 结构体:
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 文件结构没有任何分隔符,所以无论是每个数据项的的顺序还是数量,都是严格限定的,哪个字节代表什么含义,长度多少,先后顺序如何,都是不允许改变的。
接下来我们来具体学习每项的含义。
1、魔数
这是基本上每个Java开发人员的第一个Java程序:
public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } }
我使用的是Idea工具,运行,target目录下会生成对应的class文件,为了查看文件的十六进制信息,我们可以安装一个插件HexView
。
安装完成之后,选择class文件,右键 HexView,打开后的十六进制如下:
第一行中有一串特殊的字符 cafebabe
,它就是一个魔数,是 JVM 识别 class 文件的标志,JVM 会在验证阶段检查 class 文件是否以该魔数开头,如果不是则会抛出 ClassFormatError
。
这段字节很有意思——咖啡宝贝,Java原来不止是咖啡,还是宝贝😂
2、版本号
紧跟着魔数后面的四个字节 0000 0031
存储的是 class 文件的版本号:第 5 和第 6 个字节是次版本号(Minor Version),第 7 和第 8 个字节是主版本号(Major Version)。
Java的版本号是从 45 开始的,JDK1.1 之后的每个 JDK 大版本发布主版本号向上加1(JDK1.0JDK1.1使用了45.045.3的版本号),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式未发生变化。
0000 0031 对应的十进制为49,是JDK8的内部版本号。
3、常量池
紧接着主、次版本号之后的是常量池入口。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的。
如图所示,常量池容量为十六进制数0x0022,即十进制的34,这就代表常量池中有33项常量,索引值范围为1~33。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始。
常量池中主要存放两大类常量:字面量(Literal)
和符号引用(Symbolic References)
。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
这17类常量结构只有一个相同之处,表结构起始的第一位是个u1类型的标志位(tag),代表着当前常量属于哪种常量类型。
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_Moudle_info | 19 | 表示一个模块 |
CONSTANT_Package_info | 20 | 表示一个模块中开放或者导出的包 |
常量池非常繁琐,17种常量类型各自有着完全独立的数据结构,彼此之间没有什么共性和联系。
我们直接看一下常量池中的17种数据类型的结构总表:
4、访问标志
在常量池结束之后,紧接着的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 | 标志这是一个枚举 |
access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一 律为零。
5、类索引、父类索引与接口索引集合
这三者为什么放在一起呢?因为这三者用来确定类的继承关系。
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了 java.lang.Object外,所有Java类的父类索引都不为0。
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(后的接口顺序从左到右排列在接口索引集合中。
6、字段表集合
接口索引结束后,接着是字段表(field_info),它用于描述接口或者类中声明的变量——这里的字段(Field)
只包括类级变量以及实例级变量,不包括在方法内部声明的局部变量。
描述的主要信息包括:
①、字段的作用域(public,protected,private修饰)
②、是类级变量还是实例级变量(static修饰)
③、是否可变(final修饰)
④、并发可见性(volatile修饰,是否强制从主从读写)
⑤、是否可序列化(transient修饰)
⑥、字段数据类型(8种基本数据类型,对象,数组等引用类型)
⑦、字段名称
字段表的结构如下:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
access_flags是该字段的的访问标志,它和类中的访问标志很类似,用以描述该字段的权限类型:private、protected、public;并发可见性:volatile;可变性:final;
访问标志详情如下图所示:
由于Java语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。
7、方法表集合
方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,如表所示:
有区别的部分只有方法访问标志access_flag
, 因为volatile关键字和transient关键字不能修饰方法。
方法表标志位及其取值如下:
8、属性表集合
接下来终于到了最后一项:属性表集合。
前面提到的Class文件、字段表、方法表都可以携带自己的属性表集合,就是引用的这里。
属性表集合中的属性如下所示:
与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。