Java Class文件结构分析
学习 Java 虚拟机对于理解 AOP ,反射,并发同步、垃圾回收、代码优化等方面都会有不少帮助,有时候还是有必要对底层的原理做一下了解。不过看起来确实比较费解。可以一步一步来,能看懂多少是多少。
Java 虚拟机可以从 class 字节码文件、类装载体系结构、对象生命周期、执行引擎、 API 规范等几个方面来学习。 Class 文件是 java 虚拟机的基础,从 class 文件结构中可以学习到 java 虚拟机的一些基本原理。
本文主要介绍 class 文件的设计结构,为后面的话题打下一个基础。 ( 主要参考了深入 java 虚拟机这本书和网上资料 )
1. ClassFile 基本定义
Classfile 是一个连续的 8 位字节二进制流,数据项按照顺序存储在 class 文件中,相邻项没有间隔,占多字节空间的项时,高位在前。
ClassFile 文件格式是固定的,按照顺序
名称 | 长度 | 描述 | 备注 |
majic | 4 个字节 | 魔数 :0xCAFEBABE | Od –x 命令可以看到。这样保证了 Java 虚拟机能很轻松的分辨出 Java 文件和非 Java 文件 |
Minor_version 和 major_version | 分别 2 字节 | 主次版本号: Class 文件格式一旦发生变化,版本号也会随之变化。 | 如果 class 文件版本号超出了处理范围, java 虚拟机将不会处理该文件。 |
Constantpool_count,constanpool | 不固定 | 常量池:包含了文件中类和接口相关的常量。文字字符串、 final 变量值、类名和方法名的常量。常量池的大小平均占到了整个类大小的 60% 左右。 | 入口列表的形式来存储。每个常量池入口都从一个长度为一个字节的标志开始。除了字面常量还可以容纳字段名称、方法名称和类的全限名等。 |
Access_flags | 2 字节 | 访问标志 : 定义了类或接口 | 指明了是类还是接口、是抽象还是具体。公共、 final 等修饰符。 |
This_class | 2 字节 | 本身是一个常量池的索引,指向了常量池中该类全限定名的常量池入口 |
|
Super_class | 2 字节 | 指向父类全限定名 |
|
Interface_count 和 interfaces | 不固定 | 该类实现的接口数量, interfaces 包含了由该类实现的接口的常量池引用。 |
|
FiledsCount 和 fileds | 不固定 | 字段数量和字段的信息表。描述了字段的类型、描述符等。 |
|
Methods_count 和 Mechods | 不固定 | 方法总数和方法本身。使用 ASM 进行 AOP 编程,通常是通过调整 Method 中的指令来实现的。 | 每一个方法都会有一个 Mechod_info 表,改表记录了方法的方法名、描述符、返回类型。局部变量表,字节码序列等。 |
Attributes_count 和 Attributes | 不固定 | 属性总数和属性本身。写出了 |
|
2. CLassFile 详细结构
根据如上格式定义成结构体如下
( 以下内容参考了文章 http://hi.baidu.com/52dege/blog/item/f33a3cf4092491dbf3d3854f.html ) :
ClassFile {
u4 magic; // 必须为 : 0xCAFEBABE
u2 minor_version;
u2 major_version; //CLASS 文件结构主次版本号 JAVA2 支持 45.0-46.0
u2 constant_pool_count; // 记录常量信息
cp_info constant_pool[constant_pool_count-1]; // 计数从 1 开始
u2 access_flags; //class/interface 访问权限
u2 this_class; // 指向 constant_poll 中的有效索引值
u2 super_class; //0 或指向 constant_poll 中的有效索引值 , 对于 interface 必须为非 0
u2 interfaces_count; //superinterfaces 的个数
u2 interfaces[interfaces_count]; // 计数 [0,count-1) 对应 constant_pool 中的一个索引值
u2 fields_count;
field_info fields[fields_count]; // 主要用于记录 class 及实例中的变量
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
对于上面的 cp_info constant_pool[constant_pool_count-1], 是一个 cp_info 结构体的系列。
cp_info {
u1 tag;
u1 info[];
}
在 java 虚拟机规范里面一共定义了 12 种 cp_info , tag 值从 1 到 12 :
CONSTANT_Utf8 1
CONSTANT_Integer 3
CONSTANT_Float 4
CONSTANT_Long 5
CONSTANT_Double 6
CONSTANT_Class 7
CONSTANT_String 8
CONSTANT_Fieldref 9
CONSTANT_Methodref 10
CONSTANT_InterfaceMethodref 11
CONSTANT_NameAndType 12
( 好像没有 2 ???书上暂时也没看到。 )
对应的 cp_info 结构分别如下:
CONSTANT_Utf8
CONSTANT_Utf8_info {
u1 tag; //tag 值等于 1
u2 length;
u1 bytes[length];// 存储字符串数组
}
这是非常核心的一个 cp_info, 所有的字符串都由它存储表示。包括文字字符串、全限定名、字段名、方法名、描述符等都指向它。
CONSTANT_Integer
CONSTANT_Integer_info {
u1 tag; //tag 值等于 3
u4 bytes;// 按照高位在前格式存储 int 类型值
}
CONSTANT_Float
CONSTANT_Float_info {
u1 tag; //tag 值等于 4
u4 bytes;
}
CONSTANT_Long
CONSTANT_Long_info {
u1 tag; //tag 值等于 5
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Double
CONSTANT_Double_info {
u1 tag; //tag 值等于 6
u4 high_bytes;
u4 low_bytes
}
CONSTANT_Class ( 类全限定名 )
CONSTANT_Class_info {
u1 tag; //tag=7
u2 name_index; // 指向包含全限定名字符串的一个 CONSTANT_Utf8_info
}
CONSTANT_String
CONSTANT_String_info {
u1 tag; //tag=8
u2 string_index;// 包含文字字符串的 CONSTANT_Utf8_info 表索引
}
CONSTANT_Fieldref
CONSTANT_Fieldref_info {
u1 tag; //tag=9
u2 class_index; //constant_pool 的索引,对应 CONSTANT_Class_info
u2 name_and_type_index;//constant_pool 的索引,对应 CONSTANT_NameAndType_info
}
CONSTANT_Methodref
CONSTANT_Methodref_info {
u1 tag; //tag=10
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref
CONSTANT_InterfaceMethodref_info {
u1 tag; //tag=11
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_NameAndType
CONSTANT_NameAndType_info {
u1 tag; //tag=12
u2 name_index;
u2 descriptor_index;
}
access_flags 意义如下 :
ACC_PUBLIC 0x0001
ACC_FINAL 0x0010
ACC_SUPER 0x0020
ACC_INTERFACE 0x0200
ACC_ABSTRACT 0x0400
this_class: constant_pool 中的索引值 , 指向的元素的 cp_info 等价为 CONSTANT_Class_info
CONSTANT_Class_info {
u1 tag; // 必须为 CONSTANT_Class (7)
u2 name_index; // 为指向 constant_pool 中的一个索引值
}
name_index : 指向的元素的 cp_info 等价为 CONSTANT_Utf8_info
field_info {
u2 access_flags; // 访问控制权
u2 name_index; //constant_pool 中的索引,对应于 CONSTANT_Utf8_info 描述。
u2 descriptor_index; //constant_pool 中的索引,对应于 CONSTANT_Utf8_info 描述。
u2 attributes_count;
attribute_info attributes[attributes_count]; //attribute_info 将在 mothods 后描述。
}
field_info 中 access_flages 意义如下 :
ACC_PUBLIC 0x0001
ACC_PRIVATE 0x0002
ACC_PROTECTED 0x0004
ACC_STATIC 0x0008
ACC_FINAL 0x0010
ACC_VOLATILE 0x0040
ACC_TRANSIENT 0x0080
其中很显然不能同时为 ACC_FINAL 和 ACC_VOLATILE ; 且前三项是互斥的。
interface 必须置 ACC_PUBLIC, ACC_STATIC,ACC_FINAL 位,且不能置其他位。
其他未指明的位保留将来使用,并且编译器应当将其置为 0 ,同时 Java 虚拟机应当忽略他们。
methods 指明了类中的所有方法。
method_info {
u2 access_flags;
u2 name_index; // 指向 constant_pool 的入口,对应为 CONSTANT_Utf8_info
u2 descriptor_index; // 指向 constant_pool 的入口,对应为 CONSTANT_Utf8_info
u2 attributes_count;
attribute_info attributes[attributes_count];
// 此处只能出现 Code 、 Exceptions 、 Synthetic 、 Deprecated 四种类型的属性
}
access_flags 访问权描述如下 :
ACC_PUBLIC 0x0001
ACC_PRIVATE 0x0002
ACC_PROTECTED 0x0004
ACC_STATIC 0x0008
ACC_FINAL 0x0010
ACC_SYNCHRONIZED 0x0020
ACC_NATIVE 0x0100
ACC_ABSTRACT 0x0400
ACC_STRICT 0x0800
attribute_info {
u2 attribute_name_index; //constant_pool 中的索引,对应于 CONSTANT_Utf8_info 描述。
u4 attribute_length;
u1 info[attribute_length];
}
现在已经预定义的属性有 :
1. SourceFile : attribute_info 被替代为 :
SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index; // 指向 constant_pool 中的一个 CONSTANT_Utf8_info 结构。
}
2. ConstantValue : attribute_info 被替代为 :
ConstantValue_attribute {
u2 attribute_name_index;
u4 attribute_length; // 必须为 2
u2 constantvalue_index;
}
对于 constantvalue_index 意义如下 :
long CONSTANT_Long
float CONSTANT_Float
double CONSTANT_Double
int, short, char, byte, boolean CONSTANT_Integer
String CONSTANT_String
ConstantValue 用于 field_info 中,用于描述一个 static 常量 ,
且此时 field_info 的 access_flags 应为 ACC_STATIC
3. Code : attribute_info 被替代为 :
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack; // 执行此函数时可用的栈的最大深度
u2 max_locals; // 执行此函数可用到的最大本地变量数目,包括参数。
// 注意 : 一个 long/double 相当于 2 个变量数目 .
u4 code_length; // 本函数用到的代码长度。
u1 code[code_length]; // 实现本函数的真正字节码
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc; // 捕获违例时执行代码数组中的 [start_pc, end_pc) 部分
u2 handler_pc; // 现在还不大明白他是干嘛的 !!
u2 catch_type; // 指向 constant_pool 的索引,对应 CONSTANT_Class_info
}exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
CONSTANT_Class_info {
u1 tag; // 必须为 CONSTANT_Class (7)
u2 name_index; // 不用我再说了吧 ?
}
Code 属性用于 method_info 结构中。
4. Exceptions : attribute_info 被替代为 :
Exceptions_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_exceptions;
u2 exception_index_table[number_of_exceptions];
}
5. InnerClasses : attribute_info 被替代为 :
InnerClasses_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 number_of_classes;
{ u2 inner_class_info_index;
u2 outer_class_info_index;
u2 inner_name_index;
u2 inner_class_access_flags;
} classes[number_of_classes];
}
6. Synthetic : attribute_info 被替代为 :
Synthetic_attribute {
u2 attribute_name_index; // 不用废话了吧 ?
u4 attribute_length; // 必须为 0
}
Synthetic 用在 field_info 、 method_info 中,
一个没有出现在源程序中的变量必须使用 Synthetic 标记。
7. LineNumberTable : attribute_info 被替代为 :
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc; // 代码数组中的开始处
u2 line_number; // 源文件中的行号 ( 对于每一非空行都有这么一项 )
} line_number_table[line_number_table_length];
}
LineNumberTable 用于 Code 属性中,通常用于调试。
8. LocalVariableTable : attribute_info 被替代为 :
LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc;
u2 length; // 当解释到代码数组的 [start_pc,start_pc+length]
// 时变量必须被赋值 ??
u2 name_index;
u2 descriptor_index;
u2 index; // 到本地变量数组的一个索引
} local_variable_table[local_variable_table_length];
}
9. Deprecated : attribute_info 被替代为 :
Deprecated_attribute {
u2 attribute_name_index;
u4 attribute_length; // 必须为 0
}
当然也可以定义自己的属性,但要你自己的编译器和虚拟机实现。 JVM 将忽略自己不认可的属性。
以上是关于 Javaclass 文件的一个基本结构的分析。主要是对书上关于这一章的总结,同时参考了 : http://hi.baidu.com/52dege/blog/item/f33a3cf4092491dbf3d3854f.html 等网上资料。
看一个 java classfile 有助于我们理解的实际例子:
代码 1 :
运行结果为 :true true
代码 2 :
运行结果为 :false true
关于代码 1 和代码 2 的解释:
Java 编译器在生成 class 文件的时候对代码里面定义的字符串都会作为一个字符串常量存放在常量池中。
对于代码 1 中的 String a = "aaabbb"; 和 String b = "aaabbb"; 预处理在编译处理后, a 和 b 都指向了常量池里的 ”aaabbb” ,所以 a==b 是 true.
对于代码 2 中的 String b = "aaa"; b += "bbb"; 编译处理后会在常量池里面生成 ”aaa” 和 ”bbb”, 同时, b += "bbb"; 语句会导致 Jvm 去重新创建一个 string 对象,相当于 b=new String(“aaabbb”); 自然, aaa==bbb 是 false 。这也是为什么不推荐使用加号来拼接字符串了。