JVM学习--Class类文件结构
文章目录
一个例子(下面的表根据这个例子讲解)
public class test{
private int m;
public int inc(){
return m+1;
}
}
经过javac test.java
生成test.class
。使用WinHex查看如下:
Class类文件结构
Class文件格式
u1~8代表 1 ~ 8个字节的无符号数,_info代表表
类型 | 名称 | 数量 |
---|---|---|
u4 | 魔术 | 1 |
u2 | 次版本号 | 1 |
u2 | 主版本号 | 1 |
u2 | 常量池数量 | 1 |
cp_info | constant_pool | 常量池数量 - 1 |
u2 | 访问标识 | 1 |
u2 | 本类标识 | 1 |
u2 | 父类标识 | 1 |
u2 | 接口数量 | 1 |
u2 | 接口 | 接口数量 |
u2 | 字段数量 | 1 |
field_info | 字段 | 字段数量 |
u2 | 方法数量 | 1 |
method_info | 方法 | 方法数量 |
u2 | 属性数量 | 1 |
attribute_info | 属性 | 属性数量 |
魔术
标识这个文件是java文件,固定为:
CA FE BA BE
cafe babe记为咖啡宝贝。
次版本号
现在好像基本都是
00 00
主版本号
00 34
34(16进制) = 52(10进制)
版本和版本号对应如下:
Java 1.2 uses major version 46
Java 1.3 uses major version 47
Java 1.4 uses major version 48
Java 5 uses major version 49
Java 6 uses major version 50
Java 7 uses major version 51
Java 8 uses major version 52
Java 9 uses major version 53
Java 10 uses major version 54
Java 11 uses major version 55
常量池计数
需要注意的是计数是从1开始的。0代表的是某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的意思。
00 13
0x0013 -> 19 代表常量池中有19-1=18项常量,索引为1~18.
常量池
常量池中存放两大类常量:字面量(literal)和符合引用(Symbolic References)。
字面量接近于java语言层面的常量概念,如String,被声明为final的常量值等;
符合引用主要包含以下三种常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
当虚拟机运行时,需要从常量池获取对应的符号引用,再在类创建时或运行时解析并翻译到具体的内存地址中。
这些表都有一个共同的特点,就是表的第一位是一个u1类型的标志位。
Constant Type | Value | 描述 |
---|---|---|
CONSTANT_Class | 7 | 类或接口的符号引用 |
CONSTANT_Fieldref | 9 | 字段的符号引用 |
CONSTANT_Methodref | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref | 11 | 接口中方法的符号引用 |
CONSTANT_String | 8 | 字符串类型的字面量 |
CONSTANT_Integer | 3 | 整型字面量 |
CONSTANT_Float | 4 | 浮点型字面量 |
CONSTANT_Long | 5 | 长整型字面量 |
CONSTANT_Double | 6 | 双精度浮点型字面量 |
CONSTANT_NameAndType | 12 | 字段或方法的部分符号引用 |
CONSTANT_Utf8 | 1 | utf-8编码的字符串 |
CONSTANT_MethodHandle | 15 | 方法处理句柄 |
CONSTANT_MethodType | 16 | 方法类型 |
CONSTANT_InvokeDynamic | 18 | 动态方法的调用点 |
由于每一项常量表都有自己的结构,这里就不展开讲。就讲第一个
0A
0A -> 10 对照上面的表发现是类中方法的符号引用。去查对应的结构为:
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
u1一个字节的tag已经读取了,接下来是u2两个字节的类索引
00 04
这个index代表指向常量池第4项,这里不能直接得到第4项还要接着往下看。
接下来是u2两个字节的字段或方法的部分符号的索引
00 0F
这个index指向常量池第15项,目前也无法得到o(╥﹏╥)o
这里刷下花招,用javap看看。发现上面的解析并没有错误。
访问标志
访问标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public等。
Flag Name | Value | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 标识是public类型 |
ACC_FINAL | 0x0010 | 标识是final类型 |
ACC_SUPER | 0x0020 | 标识允许使用invokespecial字节码指令,JDK1.2后默认为true |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 标识抽象类型 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码生成 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
access_flags中共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位一律为0。
使用到的标识value进行 或
运算,例如test类只用到了public
修饰,所以ACC_PUBLIC 和 ACC_SUPER为真。所以值为0x0001 | 0x0020 = 0x0021
。
类索引、父类索引、接口索引集合
类索引和父类索引都是一个u2类型的数据,接口集合(包含接口数量+接口)是一组u2类型的数据的集合,Class由这3项数据确定类的继承关系。
类索引是类的全限定名。
父类索引是类的父类也就是extends。
接口数量+接口表明接口的个数和名称就是implements(如果这个类是接口则是extends)。
索引指向的是常量池里的CONSTANT_Class_info,这个索引最终会指向一个CONSTANT_Uft8_info类型的全限定名字符串。
除了java.lang.Object其他所有的父索引都不为0.
如果接口数量为0,后面的接口不再占用任何字节。
字段表集合
字段表用于描述接口或类中声明的变量。字段包含了类级变量和实例级变量,但不包括在方法内声明的变量。
字段表结构:
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
其中 access_flags 包含:这个很好理解,就不解释了。
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | Declared public; may be accessed from outside its package. |
ACC_PRIVATE | 0x0002 | Declared private; usable only within the defining class. |
ACC_PROTECTED | 0x0004 | Declared protected; may be accessed within subclasses. |
ACC_STATIC | 0x0008 | Declared static. |
ACC_FINAL | 0x0010 | Declared final; never directly assigned to after object construction (JLS §17.5). |
ACC_VOLATILE | 0x0040 | Declared volatile; cannot be cached. |
ACC_TRANSIENT | 0x0080 | Declared transient; not written or read by a persistent object manager. |
ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code. |
ACC_ENUM | 0x4000 | Declared as an element of an enum. |
name_index和descriptor_index 都是对常量池的引用,分别代表着字段的简单名称及字段和方法的描述符。简单名称指的是 没有任何修饰符的方法名或字段名,这个类中就是inc和m。
全限定名是把.
换成/
,且在最后加;
例如java.lang.String --> java/lang/String;
描述符用来描述字段的数据类型、方法的参数列表和返回值。根据描述符规则,基本数据类型及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。
FieldDescriptor:
FieldType
FieldType:
BaseType
ObjectType
ArrayType
BaseType:
B byte
C char
D double
F float
I int
J long(注意区别)
S String
Z boolean
V void返回值
ObjectType:
L ClassName ; 例如 `Ljava/lang/Object;`
ArrayType:
[ ComponentType 保留一个前置的括号例如:int[][] --> [[I
ComponentType:
FieldType
描述方法按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号里。
例如:void inc() --> ()V
再例如java.lang.String.copyValueOf(char data[], int offset, int count) ---> ([CII)Ljava/lang/String;
选定的浅蓝色就是这段内容,解读如下:1个字段,修饰符是2(private),字段名是5(m),描述是6(I).连起来就是private int m
descriptor_index;后还可能有属性表,用来描述额外的信息。如果将字段m的声明改为"final static int m = 123",那就可能存在一项名为ConstantValue的属性,值指向常量123.
字段表不会列出从父类或接口中继承来的字段,但有可能列出原来java代码中不存在的字段。
方法表集合
方法表结构和字段几乎一样。
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
方法访问标志有所差异
Flag Name | Value | Interpretation |
---|---|---|
ACC_PUBLIC | 0x0001 | Declared public; may be accessed from outside its package. |
ACC_PRIVATE | 0x0002 | Declared private; accessible only within the defining class. |
ACC_PROTECTED | 0x0004 | Declared protected; may be accessed within subclasses. |
ACC_STATIC | 0x0008 | Declared static. |
ACC_FINAL | 0x0010 | Declared final; must not be overridden (§5.4.5). |
ACC_SYNCHRONIZED | 0x0020 | Declared synchronized; invocation is wrapped by a monitor use. |
ACC_BRIDGE | 0x0040 | 是否是编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 是否接受可变参数 |
ACC_NATIVE | 0x0100 | Declared native; implemented in a language other than Java. |
ACC_ABSTRACT | 0x0400 | Declared abstract; no implementation is provided. |
ACC_STRICT | 0x0800 | Declared strictfp; floating-point mode is FP-strict. |
ACC_SYNTHETIC | 0x1000 | Declared synthetic; not present in the source code. |
方法经过编译后,方法内的代码存放在一个名为Code的属性里面,属性表是Class文件格式最具有扩展性的一种数据项目。
00 02 代表两个方法,一个方法是编译器添加的实例构造器,另一个是源码中的inc()。第一个方法的访问标识值为00 01就是public,方法名为00 07:描述索引值为00 08对应常量为()V
,属性表计数器为00 01代表有一个属性 再查看00 09发现为Code,说明该属性是方法的字节码描述。
如果父类方法在子类中没有被重写,方法表中就不会出现父类的方法信息。但是有可能出现编译器自动添加的方法,最典型的是类构造器和方法。
属性表集合
属性表限制比较宽松,而且可以自定义,篇幅超级无敌长,这里就随便拿个。。Code属性的
Code_attribute {
u2 attribute_name_index; 属性名称
u4 attribute_length; 属性值的长度(整个属性表 - 6),6是attribute_name_index+ attribute_length的长度
u2 max_stack; 操作数栈深度的最大值
u2 max_locals;局部变量表所需的存储空间
u4 code_length; 字节码长度
u1 code[code_length]; 字节码指令的字节流
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
}
exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
如果字节码从第start_pc行到第end_pc行之间出现了类型为 catch_type或其子类的异常,则转到第handler_pc行继续处理。当catch_type的值为0时,代表任何的异常情况都需要转向到 handler_pc进行处理。
参考
周志明著《深入理解java虚拟机》
https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html