class文件是一组以8字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑排列在class文件中,中间没有任何分隔符,这使得class文件中存储的内容几乎是全部程序运行的程序。
Java虚拟机规范规定,Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。
无符号数
无符号数是基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。
表
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是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_infoattributes[attributes_count];}在class文件中,主要包括
魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合。
在IDEA IDE下有一个插件可以很方便地查看class文件的结构,这个插件是jclasslib Bytecode viewer。在IDEA插件管理里面搜索安装即可,关于使用的方式,使用IDE build项目后,选择对应的java类在菜单点击view–>Show Bytecode With Jclasslib菜单即可查看。
Class的文件结构
魔数
位于class文件的头4个字节,唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件,Class文件魔数的值为0xCAFEBABE。如果一个文件不是以0xCAFEBABE开头,那它就肯定不是Java class文件。
很多文件存储标准中都使用魔数来进行身份识别,比如图片格式,如gif或jpeg等在文件头中都存有魔数。使用魔术而不是使用扩展名是基于安全性考虑的,扩展名可以随意被改变。
Class文件的版本号
紧跟在魔数的后面的4个字节是Class文件的版本号,这个版本号分为两部分
前2个字节表示次版本号(minor_version)后2个字节表示主版本号(major_version)这个版本号随着JDK版本的不同而不同,JDK1.8编译出来的class文件主版本号是52,次版本号是0。
常量池
按顺序排列下来,继魔数、版本号之后就是常量池入口,这是class文件结构中最复杂的部分。
常量池简单理解就是class文件的资源库:
1.常量池是Class文件结构中与其它项目关联最多的数据类型
2.常量池是占用Class文件空间最大的数据项目之一
3.是在文件中第一个出现的表类型数据项目
首先常量池的入口是一个U2(2个字节)的数据类型,表示常量池中常量表的个数,从1开始计数,在class文件结构中只有常量池的容量是从1开始计数的,第0位意味不引用任何一个常量池项目。
常量池主要存储字面量以及符号引用
1.字面量: 比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
2.符号引用: 属于编译原理方面的概念,包括了下面三类常量:
类和接口的全限定名
字段的名称和描述符
方法的名称和描述符
Java代码在进行Java编译的时候,并不像C和C++那样有"连接"这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址之中。
constant_pool_count:占2字节,本例为0x0016,转化为十进制为22,即说明常量池中有21个常量
constant_pool:表类型数据集合,即常量池中每一项常量都是一个表,共有14种(JDK1.7前只有11种)结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型。
访问标志(2字节)
在常量池这一大块内容后面紧跟着的则是访问标志(Access flags),这个标志主要用于识别类或者接口层次的访问信息。
主要包括:
是否final
是否public,否则是private
是否是接口
是否可用invokespecial字节码指令
是否是abstract
是否是注解
是否是枚举
access_flags一共有16个标志位可以使用,当前只定义了其中8个(JDK1.5增加后面3种),没有使用到标志位一律为0。
类索引、父类索引和接口索引集合
访问标志之后就是当前类索引、父类索引、接口索引集合,这三项数据主要用于确定这个类的继承关系。
其中类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引(interface)集合是一组u2类型的数据。(多实现单继承)
字段表集合
再往下就是字段表集合了,字段表(filed_info)用于描述接口或者类中声明的变量。
field_info{ u2 access_flags; // 修饰符标记位 u2 name_index; // 代表字段的简单名称,占2字节,是一个对常量池的引用 u2 descriptor_index; // 代表字段的类型,占2个字节,是一个对常量池的引用 u2 attributes_count; // 属性计数器 attribute_infoattributes[attributes_count];//属性表集合}字段表包含的固定数据项到descriptor_index结束,之后跟随一个属性表集合用于存储一些附加信息。
字段表集合中不会列出从父类或父接口中继承的字段,但是可能列出原本Java代码之中不存在的字段,如:内部类为了保持对外部类的访问性,自动添加指向外部类实例的字段。Java语言中字段是不能重载的,2个字段无论数据类型、修饰符是否相同,都不能使用相同的名称;但是对于字节码,只要字段描述符不同,字段重名就是合法的。
方法表集合
字段表之后,跟着的就是方法表了,起始u2类型数据表示方法数量,然后是方法表集合。
class文件存储格式对方法的描述采取了和字段描述集合完全一致的方式。
method_info{ u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_infoattributes[attributes_count];}属性表集合
最后是属性表集合,与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。
虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性。
属性名称
使用位置
含义
Code
方法表
Java代码编译成的字节码指令
ConstantValue
字段表
final关键字定义的常量值
Deprecated
类文件、字段表、方法表
被声明为deprecated的方法和字段
Exceptions
方法表
方法抛出的异常
InnerClasses
类文件
内部类列表
LineNumberTale
Code属性
Java源码的行号与字节码指令的对应关系
LocalVariableTable
Code属性
方法的局部变量描述(局部变量作用域)
SourceFile
类文件
源文件名称
Synthetic
类文件、方法表、字段表
标识方法或字段是由编译器自动生成的
最后,感谢阅读~
欢迎订阅我的微信【Seven的代码实验】订阅更多关于JVM、Spring、以及开源项目的分享,希望能让你有所收获。