class文件结构
使用sublime text等可以查看16进制文件的工具,打开任意一个.class文件,可以看到如下内容:
其中大部分内容都是看不懂的,唯一能看懂的就是最开始的8个字母cafe babe
,这个叫做“魔数”。
魔数是用来标记这个文件是什么类型的文件的。
很多类型的文件都使用了魔数,例如:
魔数之后才是class文件真正的内容。整个class文件结构大致如下:
含义解析:
结构 | 含义 |
---|---|
CA FE BA BE | 魔数(咖啡宝贝),标记该文件是一个class文件 |
副版本号 | 编译的JDK副版本号,比如1.8.0,副版本就是0 |
主版本号 | 编译的JDK主版本号,比如1.8.0,主版本号是1.8 |
常量计数器 | 该类中的常量数量 |
常量池数据 | 常量的数据,常量的数量由“常量计数器”决定 |
访问标志 | 类的访问权限,如public |
类索引 | 本类的引用信息 |
父类索引 | 父类的引用信息 |
接口计数器 | 该类中接口数量 |
接口信息数据 | 接口数据 |
字段计数器 | 该类中的字段数量 |
字段信息数据 | 字段详细信息 |
方法计数器 | 该类中的方法数量 |
方法信息数据 | 方法信息 |
属性计数器 | 属性数量 |
属性信息数据 | 属性信息,是对前面的数据的一个补充。例如方法信息区中无法保存方法体内容,则放在属性信息数据中保存 |
工欲善其事必先利其器,为了更好的学习class文件,推荐安装IDEA的class解析插件,叫jclasslib Bytecode Viewer
,安装好后,选中一个类文件,点击view->show bytecode with jclasslib 即可查看class文件信息
如果发现提示class root not found
,只需要执行一下main方法,让代码编译一下即可。
常量池结构
类中常量的数量是不一定的,因此有一个常量计数值(constant_pool_count)来表示常量的数量
这个数值有点特殊,它是从1开始计数,假设这个值是20,那么实际常量的数量其实是19
常量池中主要有两大类常量:
- 字面量:Java语言层面的常量概念,比如final修饰的常量,字符串等
- 符号引用:类、接口、字段、方法引用等
常量的数据结构类型主要有:
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8字符串 |
CONSTANT_Integer_info | 3 | int常量 |
CONSTANT_Float_info | 4 | float常量 |
CONSTANT_Long_info | 5 | long常量 |
CONSTANT_Double_info | 6 | double常量 |
CONSTANT_Class_info | 7 | 类或接口引用 |
CONSTANT_String_info | 8 | 字符串常量 |
CONSTANT_Fieldref_info | 9 | 字段引用 |
CONSTANT_Methodref_info | 10 | 方法引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的名称和类型 |
CONSTANT_MethodHandle_info | 15 | 方法句柄 |
CONSTANT_MethodType_info | 16 | 方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 动态方法调用点 |
以上类型的常量,都有各自不同的数据结构,使用标志tag来标记是哪一种常量,举几个例子:
CONSTANT_Integer_info{
u1 tag=3;
u4 bytes;
}
该结构中:
- u1中的1,表示无符号的1个字节,u4则表示4个字节
- tag=3,对照上面的表格,说明这是int常量
- bytes则是int的值
类似,CONSTANT_Float_info
有着和CONSTANT_Integer_info
几乎一样的结构
CONSTANT_Float_info{
u1 tag=4;
u4 bytes;
}
double和long因为占用8个字节,因此和int、float有些许不同,例如:
CONSTANT_Long_info{
u1 tag=6;
u4 high_bytes;
u4 low_bytes;
}
long和double的数据有高位字节和低位字节
CONSTANT_String_info
的结构也不一样:
CONSTANT_String_info{
u1 tag=8;
u2 string_index;
}
String类型常量结构并不直接存储字符串,而是保存了一个索引,指向CONSTANT_Utf8_info
:
CONSTANT_Utf8_info{
u1 tag=1;
u2 length;
u1 bytes[length]
}
其中length
表示字符串的字节数,bytes[length]
则是字符串的字节数组
类似的,class类型的常量也是保存了一个引用:
CONSTANT_Class_info{
u1 tag=7;
u2 name_index;
}
name_index
指向了一个字符串CONSTANT_Utf8_info
,该字符串保存了类的全限定名,即含包名的完整类名,如java/lang/Object
。
与之类似的,类中的方法和字段也使用了这种方法来表示
那么哪些字面量类型的值会进入常量池呢?
- final修饰的基本数据类型会进入常量池
- 非final类型的值,只有double,float,long类型的才会进入常量池
- 常量池中包含的字符串类型字面量会进入常量池(双引号的字符串值)
可以使用jclasslib Bytecode Viewer
查看常量池情况,例如下方的类,可以和上面提到的三种情况一一对照
访问标记
访问标记比较简单,有以下几种:
名称 | 含义 | 名称 | 含义 |
---|---|---|---|
ACC_PUBLIC | 是否为public | ACC_ABSTRACT | 是否抽象 |
ACC_FINAL | 是否为final | ACC_SYNTHETIC | 不是用户代码生成 |
ACC_SUPER | 始终为true,没啥用 | ACC_ANNOTATION | 是否是注解 |
ACC_INTERFACE | 是否是接口 | ACC_ENUM | 是否是枚举 |
类索引
类索引包含本类索引,父类索引,接口索引,它们都是一种符号引用,指向了常量池中对应的CONSTANT_Class_info
字段
字段包含几个部分:
- 访问标记,如public,private
- 字段名
- 描述符
字段访问标记:
标记 | 含义 | 标记 | 含义 |
---|---|---|---|
ACC_PUBLIC | 是否public | ACC_VOLATILE | 是否volatile |
ACC_PRIVATE | 是否private | ACC_TRANSIENT | 是否transient |
ACC_PROTECTED | 是否protected | ACC_SYNTHETIC | 是否由编译器自动产生 |
ACC_STATIC | 是否static | ACC_ENUM | 是否是枚举 |
ACC_FINAL | 是否final |
字段名:一种符号引用,指向常量池
描述符,用于表示字段的类型:
描述符标志 | 含义 | 描述符标志 | 含义 |
---|---|---|---|
B | byte | J | long |
C | char | S | short |
D | double | Z | boolean |
F | float | V | void |
I | int | L | 对象类型,如Ljava/lang/Object |
数组类型的,则使用符号[
表示,n维数组就有n个[
,例如:
String[]
的描述符为[Ljava/lang/Object
float[][]
的描述符为[[F
方法
方法和字段的结构一致,也是包含访问标记、名称、描述符
方法访问标记
标记 | 含义 | 标记 | 含义 |
---|---|---|---|
ACC_PUBLIC | 是否public | ACC_BRIDGE | 是否由编译器产生的桥接方法 |
ACC_PRIVATE | 是否private | ACC_VARARGS | 是否接受不定参数 |
ACC_PROTECTED | 是否protected | ACC_NATIVE | 是否为native |
ACC_STATIC | 是否static | ACC_ABSTRACT | 是否abstract |
ACC_FINAL | 是否final | ACC_STRICTFP | 是否stricfp |
ACC_SYNCHRONIZED | 是否synchronized | ACC_SYNTHETIC | 是否由编译器自动产生 |
方法名:一个符号引用,指向常量池字符串
描述符:方法的描述符,基本格式为(方法参数1;方法参数2)返回值
,例如以下方法:
private int myMethod(String[] args, int args2){
return 0;
}
对应的描述符为([java/lang/String;I)I
奇怪的是,方法区里只有方法的签名,却没有方法的具体代码。
其实方法的代码,被编译成字节码之后保存在属性信息数据中了
属性
字段、方法等数据,都可以携带自己的属性,例如方法的源码,则是编译成字节码保存在名为Code
的属性中