使用Java编译器可以将Java代码编译为存储字节码的Class文件,JVM上可以运行字节码文件,事实上,其他的语言也可以利用其相应的编译器编译成Class文件运行在Java虚拟机上。
Class文件是一组以8位字节为基础单位的二进制流,结构中只有两种数据类型:无符号数和表。
无符号数以u1、u2、u4、u8来表示1、2、4和8个字节的无符号数,可以来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值;
表是由多个无符号数或者其他表作为数据项构成的复合数据类型,习惯以“_info”结尾。整个Class文件本质上就是一张表
以以下简单的Java类为例
package classFile; public class SubClass implements AInterface{ private Double number; public int fun(int a ){ return a ; } }
AInterface是一个空的接口,方便后面分析;类里只有一个私有的实例变量和一个简单的方法。
编译之后生成Class文件用16进制读取后
1.魔数
Class文件的前4个字节称为魔数,用来确定这个文件是否是能被虚拟机接受的Class文件。0xCAFEBABE,(咖啡宝贝?)
2.Class文件版本号
紧接着第5和第6个字节是次版本号,第7和第8个字节是主版本号。图上0x00000033对应这JDK1.7.0,这个有相应的对照表
3.常量池
版本号之后紧接着是常量池,可以理解为Class文件中的资源仓库,因为常量池的数据数量是不固定的,因此常量池的入口需要防止一项u2类型数据,记录常量池容量。这里的技术是从1开始,0留给不引用任何常量池时使用。
图中0x0018代表常量池的容量,换算成十进制就是23个,注意从1开始
常量池中主要存储两类常量:字面量和符号引用。字面量如文本自负、生命为final的常量值,符号引用包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
常量池中的每一个常量都是一个表,Jdk1.7加入了3种类型后,一共有14种类型,譬如CONSTANT_Class_info表的结构
类型 | 名称 |
u1 | tag |
u2 | name_index |
tag是一个标志类型的标志位,CONSTANT_Class_info为7,14中类型中都有这一标志位;name_index是一个索引值,一般指向CONTAST_Utf8_info类型常量,代表这个类的全限定名。
CONTAST_Utf8_info的表结构如下所示
类型 | 名称 |
u1 | tag |
u2 | length |
u1 | bytes |
利用javap命令可以分析字节码文件,在dos窗口输入javap -verbose SubClass 可以看到(图中只截取了常量池部分)
根据标号可以看出总的数量为23,也就对应十六进制中的0x0018,索引为1的是一个CONSTANT_Class_info类型的表,name_index指向索引为2的CONTAST_Utf8_info表,表中对应的字符串也就是类的全限定名:classFile/SubClass
关于常量池的14中类型以及表结构不再赘述。
4.访问标志
常量池结束后,紧接着两个字节代表访问标志,用于标识类或接口层次的访问信息。两个字节16位目前只定义了其中八位。
标志名称 | 标志值 | 含义 |
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 | 标识是一个枚举 |
0x0021即为public修饰,另外ACC_SUPER在JDK1.0.2编译后的都为真(其实就是区别1.0.2前后)
5.类索引、父类索引和接口索引集合
类索引和父类索引都是一个u2类型的数据,接口索引是一个u2类型的集合(接口可以多个实现),一起确定这个类的继承关系。
在访问标志之后,紧跟的是类索引和父类索引,各自通过u2类型的索引表示,索引指向常量池中的CONSTANT_Class_info类型,而又进而指向CONTAST_Utf8_info中的全限定名字符串。
而紧接着u2类型为接口的计数器,表示实现了几个接口,再往后依次指向各个接口,没有实现接口的时候,计数值为0。
图中的0x01 0x03 0x05 从常量池中可以看出,最后都指向类或接口的全限定名
6.字段表集合
字段表用于描述接口或者类中声明的变量。包括类变量和实例变量,不包括方法内部的局部变量。
字段包括的信息有作用域,和各种修饰符加上字段的名称
字段表的结构如下
类型 | 名称 |
u2 | access_flags |
u2 | name_index |
u2 | descriptor_index |
u2 | attributes_count |
attribute_info | attributes |
access_flags和类的访问标志类似,放着字段的访问标志。
紧跟着name_index放这字段的简单名称(也就是字段的名字,区别于全限定名)。
descriptor_index放着字段的描述符。描述符作用是来描述字段的数据类型、方法的参数列表和返回值
标识字符 | 含义 | 标识字符 | 含义 |
B | byte | J | long |
C | char | S | short |
D | double | Z | boolean |
F | float | V | void |
I | int | L | 对象类型 |
对于数组,每一维度用一个"["标识,譬如 String[][]--[[Ljava/lang/String int[]---[I
对于方法,按照参数列表,返回值的顺序,参数列表放在一个小括号里,譬如 void fun(int i, float f)---(IF)V
常量池中索引为8的分别代表定义的实例变量和方法的描述符。
字段表后面的两个指其携带的属性表信息。
7.方法表集合
方法表和字段表集合除了修饰的标志位不一样,其他的项基本一样
而方法的内部代码存储在其携带的“Code”属性表里
在Java的重载中,除了要求方法打的简单名称相同之外,还需要有不同的特征签名,在Java代码层面特征签名不包括返回值,而在字节码层面则包括返回值,所以在Class文件中,仅仅返回值不同的两个方法也可以共存。
问题:书上说特征签名是一个方法中各参数在常量池中的字段符号引用的集合,因为返回值不会包含在特征签名中,所以Java中无法仅仅通过返回值的不同对方法进行重载,但是方法返回值不同,其描述符不同,特征签名也不同啊?
8.属性表集合
属性表类型较多,例如Code属性用于描述方发表中的方法体编译成的字节码文件;ConstantValue属性用于描述字段表中final修饰的常量的值。不一一列举了。