Class文件
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8为字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值
- 表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合。
注意:Class的结构不像XML等描述语言,由于它没有任何分隔符号,所以在表6-1中的数据项,无论是顺序还是数量,甚至于数据存储的 字节序(Byte Ordering,Class文件中字节序为Big-Endian) 这样的细节,都是被严格限定的。
一、魔数与class文件的版本
1、魔数
定义:每个Class 文件的头4个字节称为魔数(Magic Number)
作用:确定这个文件是否为一个能被虚拟机接受的Class文件,使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件名的扩展名可以随意更改。
Class文件的魔数值:0xCAFEBABE (咖啡宝贝?)
2、版本号
定义:紧跟着魔数的第4个字节存储的是Class文件的版本号—第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)
注意:java的版本号是从45开始的,jdk1.1之后的每个jdk大版本发布主版本号向上加1;高版本的jdk能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
二、常量池
紧紧跟着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。
注意:由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一个u2类型的数据,代表常量池容量计数值(constant_poll_count)。与java中语言习惯不一样的是,这个容量计数器是从1而不是从0开始的。这样做的目的在于满足后面某些指向常量池的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以吧索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始的。
常量池中主要存放两大常量
1、字面量
比较接近于java语言层面的的常量概念,如文本字符串、声明为final的常量值等
2、符号引用
1、类和接口的全限定名(Fully Qualified Name)
2、字段的名称为描述符(Description)
3、方法的名称和描述符
注意:在Class文件中不会保存各个方法、字段的最终内存信息,因此这些字段、方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在累创建时或运行时解析、翻译到具体的内存地址之中。
CONSTANT_Class_info
tag是标志位,用于区分常量类型;name_index是一个索引值,它指向常量池中一个CONSTATNT_Utf8_info类型常量,此常量代表了这个类(或者接口)的全限定类名
CONSTATNT_Utf8_info
length值说明了这个utf-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用utf-8缩略编码表示的字符串。
utf-8缩略编码与普通utf-8编码的区别:
从‘\u0001’到’\u007f’之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从‘\u0080’到‘\u07ff’之间的所有字符的缩略编码用两个字节表示,从‘\u0800’到’\uffff’之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。
注意:由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,所以CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。所以Java程序中如果定义了超过64KB英文字符的变量或方法名,将无法编译。
三、访问标志
定义:在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型等
注意:access_flags中一共有16个标志位可以用,当前只定义了其中8个,没有使用的标志位要求一律为0。
四、类索引、父类索引与接口索引
定义
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。
所有的java类都有父类,因此除了java.lang.Object外,所有java类的父类索引都不为0。
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用连个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Class_info类型的常量中的全限定名字符串。
对于接口索引集合,入口的第一项——u2类型的数据为接口计数器(interface_count),表示索引表的容量。如果类没有实现任何接口,则该计数器的值为0,后面的几口索引表不再占用任何字节。
五、字段表集合
定义
字段表(filed_info)用于描述接口或者类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
字段包含的信息
作用域(public、private等修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(violate修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称
上述信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合标志位来表示。而字段叫什么名字、字段被定义为什么数据类型,这些是无法固定的,只能引用常量池中的常量来描述。
access_flags
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型。
name_index和descriptor_index
跟随access_flags标志,它们是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。
简单名称 & 描述符 & 全限定名
全限定名:“org/wyl/clazz/TestClass”是这个类的全限定名,仅仅是把类名中的“.”替换称“/”而已。为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加一个“;”表示全限定名结束
简单名称:是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别为“inc” 和 “m”。
描述符:描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名类表示
对于数组类型,每一个维度将使用一个前置的“[”字符来描述,如一个定义为“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”
字段表都包含的固定数据项目到descriptor_index为止就结束了,不过在descriptor_index之后更随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述零至多项的额外信息。
注意:字段表集合中不会列出从超类或者父类接口中继承而来的字段,但可能列出原本java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。在java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但对于字节码来讲,如果两个字段的描述符不一致,那字段名就是合法的。
六、方法表集合
定义
Class文件存储格式中对方法的描述与对字段的描述几乎采用的是完全一致的方式,方法表的结构如同字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(description_index)、属性集合(Attributes)几项。
方法里面的java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里面。
注意:与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“”方法和实例构造器“”方法
在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。
七、属性表集合
定义
属性表(Attribute_info)在Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场合专有的信息。与其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,java虚拟机运行是会忽略掉它不认识的属性。
对于每个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。
code属性
java程序方法体中的代码经过javac编译器处理后,最终变为字节码指令存储在Code属性中。Code属性出现在方法表的属性集合中。
attribute_name_index
是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称
attribute_length
指示了属性的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表的长度减去6个字节
max_stack
代表了操作数栈深度的最大值。虚拟机运行时需要根据这个值来分配栈帧中的操作栈深度
max_locals
代表了局部变量表所需的存储空间。max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。
code_length
code_lenght和Code用来存储java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。
注意:code_length虽然它是一个u4类型的长度值,理论上最大值可以达到2的32次方-1,但是虚拟机规范中明确限制了一个方法不允许超过65535条字节码指令,既它实际只使用了u2的长度,如果超过这个限制,javac编译器会拒绝编译。
code
Code属性是Class文件中最重要的一个属性,如果吧一个java程序中的信息分为代码(Code,方法体里面的java代码)和元数据(metadata,包括类、字段、方法定义及其他信息)两部分,那么在整个Class文件中,code属性用于描述代码,所有的其他数据项目都用于描述元数据。
–以上内容摘录自《深入理解Java虚拟机》