Class类文件结构
平台无关性如图,源码经过编译得到的字节码文件可以由运行Java虚拟机的机器运行,因此可以说字节码是Java语言跨平台的基石,同样也是其他语言跨平台的有效途径只要目标语言的编译器按照Java字节码存储规范进行编译,那么得到的class文件都可以被正确执行Class文件结构Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔
平台无关性
- 如图,源码经过编译得到的字节码文件可以由运行Java虚拟机的机器运行,因此可以说字节码是Java语言跨平台的基石,同样也是其他语言跨平台的有效途径
- 只要目标语言的编译器按照Java字节码存储规范进行编译,那么得到的class文件都可以被正确执行
Class文件结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据。根据Java虚拟机规范的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表
无符号数属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。那么表是干嘛的呢?表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的
整个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 | attribute_count(附加属性的个数) | 1 |
attribute_info | attributes(附加属性的表) | attributes_count |
从表中可以看出,无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的该数据项的形式,称这一系列连续的摸一个类型的数据为某一类型的集合,比如,fields_count个field_info表数据构成了字段表集合。这里需要说明的是:Class文件中的数据项,都是严格按照上表中的顺序和数量被严格限定的,每个字节代表的含义,长度,先后顺序等都不允许改变
在class文件中,主要分为魔数、Class文件的版本号、常量池、访问标志、类索引(还包括父类索引和接口索引集合)、字段表集合、方法表集合、属性表集合。下面就分别对每一种文件进行说明
魔数与Class文件版本号
每个class字节码文件头4个字节为魔数,作用是用于确定该文件是否能被虚拟机接受,固定为0xCAFEBABE
第5和第6个字节是次版本号,第7和第8是主版本号。因此,字节码文件的版本号确定了能执行该程序的虚拟机JDK版本。
用Sublime text打开自己的一个class文件,主版本号是50,因此要求JDK至少是1.6以上版本
cafe babe 0000 0032 00f1 0a00 3d00 7707
0078 0800 790a 0002 007a 0700 7b0a 0005
0077 0800 7c0a 0005 007d 0800 7e0a 0005
007f 0a00 8000 810a 001a 0082 0a00 1a00
830a 001a 0084 0700 850a 000f 0077 0800
860b 0087 0088 0700 890b 008a 008b 0800
8c08 008d 0800 8e07 008f 0800 9007 0091
0700 9207 0093 0700 940a 001d 007a 0a00
1c00 950a 001b 0096 0a00 1b00 9708 0098
0a00 9900 9a08 009b 0800 9c0a 001a 009d
常量池
常量池可以简单理解为class文件的资源从库,这种数据类型是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的项目之一
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用总结起来则包括了下面三类常量:
类和接口的全限定名(即带有包名的Class名,如:org.lxh.test.TestClass)
字段的名称和描述符(private、static等描述符)
方法的名称和描述符(private、static等描述符)
虚拟机在加载Class文件时才会进行动态连接,也就是说,Class文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中
符号引用和直接引用的区别与关联
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中
直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了
在常量池中每一项常量都是一个表,在jdk1.7中共有14中常量类型,所以常量池的项目就对应14张表,这14张表的每种类型都不一样。但是有一个共同特点:表开始的第一位都是一个u1类型的标志位,代表这个常量属于哪种类型
类型 | 标记 | 描述 |
---|---|---|
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 | 字段&方法部分符号引用 |
- 需要注意的是,在Class文件中,方法、字段都需要引用CONSTANT-Utf8_info类型的常量,所以这种类型的常量的长度有一定的限制,也就是Java中方法、字段的最大长度。在CONSTANT-Utf8_info中,其length的值u2,说明Java虚拟机只能编译最大大约64KB的变量或者方法名。超过的话将不会进行编译
访问标志
- 常量池之后的数据结构是访问标志(access_flags),这个标志主要用于识别一些类或者接口层次的访问信息,主要包括:这个Class是类还是接口、是否定义public、是否定义abstract类型;如果是类的话是否被声明为final等。具体的标志访问如下:
类索引、父类索引和接口索引集合
- 这个数据项主要用于确定这个类的继承关系。其中类索引和父类所以都是一个u2类型的数据,而接口索引集合是一组u2类型的数据。在Java中由于不允许多继承,所以父类索引是唯一的,但是一个类可以实现多个接口,所以得到的接口索引是一个集合,表示这个类实现了哪些接口
字段表集合
字段表用于描述接口或者类中声明的变量。字段包括类级变量和实例级变量,但是不包括方法内部声明的局部变量(这些变量是存储在Java虚拟机栈中的局部变量表中的)。自然,描述一个字段的信息包括:字段的作用域(public、protected、private)、实例变量与否(static)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本数据类型、对象、数组)、字段名称。字段的信息也被存放在一张表中,其字段表包括三种类型:
u2类型访问标志(access_flags)
u2类型的name_index(字段的简单名称)
u2类型的描述符(descriptor_index)
其访问标志在access_flags中,如下图所示:
- 上面出现了简单名称,上文中出现了全限定名,以及这里出现的描述符,三者有什么区别呢?其中全限定名称比较好理解,就是类的完整路径信息。而简单名称则是指没有类型和参数修饰的方法或者字段名称,比如一个方法如下:
public void inc(int a,int b){
System.out.println(a+b);
}
那么这个方法的简单名称就是inc。
- 相对于以上两者,描述符相对复杂一些。描述符的主要的作用是描述字段的数据类型、方法的参数列表和返回值。其中我们熟悉的void,在Class文件中用V表示。下面是完整的描述符标志的含义:
对于数组类型,每一维度使用一个前置的“[”字符描述,如果是二维数组,那么就有两个“[”符号。比如“java.lang.String[][]”会被记录成“[[Ljava.lang.String;”
对于方法,则是按照县参数列表后返回值的顺序进行描述的。比如方法int inc(int a,int[] b,char[][] c,int d)的描述符是“(I[I[[CI)I”
方法表集合
JVM中堆方法表的描述与字段表是一致的,包括了:访问标志、名称索引、描述符索引、属性表集合。方法表单额结构与字段表是一致的,区别在于访问标志的不同.在方法中不能了用volatile和transient关键字修饰,所以这两个标志不能用在方法表中。在方法中添加了字段不能使用的访问标志,比如方法可以使用synchronized、native、strictfp、abstract关键字修饰,所以在方法表中就增加了相应的访问标志
要注意的是,如果父类方法没有在子类中重写,那么在方法中不会自动出现来自父类的方法信息。同样的,有可能添加编译器自动增加的方法,比如方法
属性表集合
- 前面的Class文件、字段表和方法表都可以携带自己的属性信息,这个信息用属性表进行描述,用于描述某些场景专有的信息。在属性表中没有类似Class文件的数据项目类型和顺序的严格要求,只要新的属性不与现有的属性名重复,任何人都可以向属性表中写入自己定义的属性信息
Code属性
- Java程序方法体中的代码经过javac编译最终编译成的字节码指令就保存在Code属性中。但是并非所有的方法表都必须存在这个属性。Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code)和元数据(Metadata,包括类、字段、方法定义及其其他信息)两部分,那么在整个Class文件中,Code属性用于描述代码,所有其他的数据项目都用于描述元数据
Exceptions属性
- 这个属性的作用是列举出方法中可能抛出的受查异常(Checked Exception),也就是描述throws 后的列举的异常
LineNumberTable属性
- 主要用于描述Java源代码行号与字节码行号之间的对应关系。这个属性也不是必须的。如果没有这个属性,对程序的直接影响就是当抛出异常的时候无法显示对应的行号;并且在调试的时候无法通过设置断点的方法是调试程序
LocalVariableTable属性
- 用于描述栈帧中局部变量表中的变量与Java源码中定义的变量的之间的关系。也不属于必须的属性。如果没有这个属性,产生的直接影响就是当别人引用这个方法的时候,所有的参数名称都会丢失,IDE将会使用诸如args0、args1之类的参数进行显示。自然,当调试程序的时候,显示的参数名称是不可知的
SourceFile属性
- 用于记录这个Class文件的源码文件名称。如果不使用这个属性,那么当抛出异常的时候,堆栈中将不会显示出错代码所属的文件名
ConstantValue属性
作用是通知虚拟机自动为静态变量赋值。要注意的是,只有被static关键字修饰的额变量才可以使用这个属性(类变量)。对于非类变量,初始化是在方法中进行的;对于类变量可以选择两种方式进行变量的初始化:一是在类构造器方法中使用;二是是ConstantValue属性。目前Sun Hotspot的选择原则是:如果一个变量同时使用static和final关键字修饰,并且这个变量是基本数据类型或者java.lang.String类型的话,就使用ConstantValue属性进行初始化。如果没有被final修饰或者并非是基本数据类型,那么将会选择使用方法进行初始化
InnerClass属性
- 这个属性主要用于记录内部类与宿主类之间的关联关系
Deprecated以及Synthetic属性
这两个属性都属于标志类型的布尔属性,只存在有没有的区别。
Deprecated属性用于表示某个类、字段或者方法,已经被程序作者定为不再推荐使用,可以通过注解@deprecated实现
Synthetic属性代表此字段并不是由Java源码产生的,而是通过编译器自行添加的
StackMapTable属性
- 该属性的目的在于代替以前比较消耗性能的基于数据流分析的类型推导验证器
Signature属性
- 这个属性是专门用来记录泛型类型的,因为在Java语言采用的是擦除法实现的泛型,在字节码(Code属性)中,泛型信息编译之后会被擦除。擦除法的优点是能够节省泛型所占的内存空间,缺点是在运行期间无法通过反射得到泛型信息,而Signature属性则弥补了这一缺陷。现在的Java反射API已经能够得到泛型信息,功劳就在于这个属性
BootstrapMethods属性
- 这个属性用于保存invokedynamic指令引用的引导方法限定符。(该指令用于在运行时动态解析出调用点限定符所引用的方法,并执行该方法)
更多推荐
所有评论(0)