Class文件是一组以8位字节为基础单位的二进制流,各数据项严格按顺序排列其中,中间没有添加任何分隔符.
根据JAVA虚拟机规范的规定,CLASS文件格式采用一种类似C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1,u2,u4,u8来分别表示一个字节,两个字节,四个字节和8个字节的无符号数,无符号数用来描述数字,索引引用,数量值或按照UTF8编码构成字符串数
表是由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性的以"_info"结尾,表用于描述有层次关系的复合结构的数据。整个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 | field_count | 1 |
field_info | fields | field_count |
u2 | method_count | 1 |
method_info | methods | method_count |
u2 | attribute_count | 1 |
attribute_info | attributes | attribte_count |
魔数(magic)(4个字节)
0xCAFEBABE
次版本号(2个字节)
主版本号(2个字节)
高版本号的JDK可以向下兼容,但不能运行版本高于自己的CLASS文件。
常量池计数器(2个字节)
计数器从1而不是0开始,0x0016,十进制为22,代表有21个常量。索引为1~21.没有使用0索引是因为在后面某些指向常量池的索引可以通过0索引表示不引用任何一个常量池项目的意思。
常量池
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)
字面量的例子有文本字符串,被声明为final的常量值等。
符号引用包含三类常量:
使用JAVAP工具可以分析class文件字节码
javap -verbose TestClass
访问标志
用于识别一些类或接口的访问信息,包括这个CLASS是类还是接口,是否为public,是否为abstract等
类索引、父类索引与接口索引集合
用来确定继承关系
字段表集合
用于描述接口或类中声明的变量。字段field包括了类级变量或实例级变量,但不包括在方法内部声明的变量。不包括方法内部声明的变量.描述了该字段是否是Public,private, protected, static,final,volatile,transient等
描述符用来描述字段的数据类型,方法的参数列表和返回值.用表示标识字符表示,对象类型用L+对象的全限定名表示
方法表集合
描述方法
包括访问标志,名称索引,描述符索引,属性表索引。方法中的代码储存在方法属性表集合中一个名为Code的属性中。
在JAVA虚拟机规范中,要重载一个方法,除了要与原方法具有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名,特征签名在JAVA代码层面和字节码层面有不同的定义。代码层面的签名只包括方法名称,参数顺序及参数类型,字节码层面还包括方法返回值和异常表。因此在CLASS文件中,如果两个方法有相同的名称和特征签名,但返回值不同,是合法的。
属性表集合
属性表集合的限制较少,不要求各个属性表有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息。JAVA虚拟机在运行时会忽略掉它不认识的属性。JAVA虚拟机规范中预定义了9项虚拟机实现应当能识别的属性:
属性名称 | 使用位置 | 含义 |
Code | 方法名 | JAVA代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类,方法表,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类,方法表,字段表 | 标识方法或字段为编译器自动生成的 |
Code属性
JAVA程序方法体中的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性中。
max_stack代表操作栈深度的最大值,在方法执行的任何时刻,操作数栈都不会超过这个深度,虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度。
max_locals代表局部变量表所需的存储空间,单位为slot,对于byte,char,float,int,short,boolean,reference和returnAddress每个局部变量占用一个slot,而double和long需要两个slot.
并不是方法中用到了多少个局部变量,就把这些局部变量所占的Slot之和作为max_locals的值,原因是局部变量表中的slot可以重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占用的slot就可以被其他局部变量所使用。这个值编译器会自动计算得出
code_length和code用来存储java源程序编译后生成的字节码指令,code_length代表字节码长度,code用于存储字节码指令的一系列字节流。字节码的每个指令就是一个字节。这样可以推出,一个字节最多可以代表256条指令, 目前已经使用了约200条。
而code_length有4个字节,所以一个方法做多允许有65535条字节码指令,如果超过这个限制,javac就会拒绝编译,一般JSP可能会这个原因导致失败。
在任何实例方法中,都可以通过this关键字访问到此方法所属的对象,它的底层实现就是通过javac编译器在编译的时候把this关键字的访问转变为对一个普通方法参数的访问。
因此,任何实例方法的参数Args_size最少是1,而且locals最少也是1.而静态方法就可以为0了。
异常表实际是Java代码的一部分,从start_pc行到end_pc行出现了类型为catch_type的异常,就转到第handler_pc行处理。这四个参数就组成了异常表。
对于finally的实现,实际上就是对catch字段和前面对于任意情况都运行的异常表记录
Exceptions属性
表示方法可能抛出number_of_exceptions种受查异常,每种受查异常使用一个exception_index_table项表示
LineNumberTable属性
用于描述java源代码行号与字节码行号直接的对应关系。可以用-g:none或-g:lines选项来取消或要求生成这项信息,主要影响是报错时对战是否显示出错的行号。同时debug时无法设置断点。
LocalVariableTable属性
用于描述栈帧中局部变量表中的变量与源码中定义的变量之间的关系。也可以选择开关,关闭后果就是报错时看不到变量名称
SourceFile属性
用于记录生成这个Class文件的源码文件名称。可选
ConstantValue属性
通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量才可以使用这项属性。
在JAVA中,int x=123;和static int x=123;的区别在于,非static类型的变量(实例变量)的赋值是在实例构造器<init>方法中进行的;而对于静态变量,则有两种方式可以选择:在类构造器<clinit>方法中进行,或者使用ConstantValue属性来赋值。目前SUN JAVAC编译器的选择是如果同时使用final和static来修饰一个变量,并且这个变量的数据类型是基本类型或者String的话,就生成ConstantValue属性来进行初始化,如果这个变量没有用final修饰,或者非以上类型,则选择在<clinit>中进行初始化。
InnerClasses属性
用于记录内部类与宿主类之间的关系
Deprecated和synthetic属性
都属于标志类布尔属性。deprecated表示在代码中使用@deprecated注释进行设置。synthetic表示字段或方法不是java源码产生,而是编译器自行添加的。