一个class文件由一个单一的classfile构成
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
以下面这段代码作为示例,分析class文件结构
package org.fenixsoft.clazz;
public class TestClass {
private int m;
public int inc() {
return m + 1;
}
}
使用winhex打开编译后的class文件,内容如下
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为
一个能被虚拟机接受的Class文件, 值为 0xCAFEBABE
魔数之后的四个字节为版本号,第5和第6个字节是次版本号(MinorVersion),第7和第8个字节是主版本号(Major Version)。我的jdk是1.8.0_73 , 次版本号为ox0000(从jdk1.2开始,次版本号固定位0), 主版本号为ox0034,转换为10进制就是52
版本号后面是常量池, 首先是常量池容量计数值, 这个容量计数是从1而不是0开始的,如下图所示,常量池容量(偏移地址:0x00000008)为十六进制数0x0016,即十进制的22,这就
代表常量池中有21项常量,索引值范围为1~21
常量池表格式如下
cp_info {
u1 tag;
u1 info[];
}
常量池的项目类型, 17种常量类型各自有着完全独立的数据结构
| 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 |
| CONSTANT_MethodHandle | 15 |
| CONSTANT_MethodType | 16 |
| CONSTANT_InvokeDynamic | 18 |
常量池的第一项常量 它的标志位是ox0A,查表可知这个常量属于CONSTANT_Methodref
CONSTANT_Methodref结构如下
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
tag是标志位,为10
class_index 指向声明方法的类标识符CONSTANT_Methodref的索引项,此处为ox0004
name_and_type_index 是指向名称及类型标识符 CONSTANT_NameAndType的索引项,此处为ox0012
对class文件使用 javap 可以看到所有的常量
javap -verbose TestClass 警告: 二进制文件TestClass包含org.fenixsoft.clazz.TestClass Classfile /D:/thkj/spring/out/production/spring/org/fenixsoft/clazz/TestClass.class Last modified 2021-12-14; size 393 bytes MD5 checksum ca6d8b2868083171147fd4edb61a731b Compiled from "TestClass.java" public class org.fenixsoft.clazz.TestClass minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#18 // java/lang/Object."":()V #2 = Fieldref #3.#19 // org/fenixsoft/clazz/TestClass.m:I #3 = Class #20 // org/fenixsoft/clazz/TestClass #4 = Class #21 // java/lang/Object #5 = Utf8 m #6 = Utf8 I #7 = Utf8 #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Lorg/fenixsoft/clazz/TestClass; #14 = Utf8 inc #15 = Utf8 ()I #16 = Utf8 SourceFile #17 = Utf8 TestClass.java #18 = NameAndType #7:#8 // " ":()V #19 = NameAndType #5:#6 // m:I #20 = Utf8 org/fenixsoft/clazz/TestClass #21 = Utf8 java/lang/Object { public org.fenixsoft.clazz.TestClass(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object." ":()V 4: return LineNumberTable: line 3: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lorg/fenixsoft/clazz/TestClass; public int inc(); descriptor: ()I flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field m:I 4: iconst_1 5: iadd 6: ireturn LineNumberTable: line 6: 0 LocalVariableTable: Start Length Slot Name Signature 0 7 0 this Lorg/fenixsoft/clazz/TestClass; } SourceFile: "TestClass.java"
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags)
TestClass是一个普通Java类,不是接口、枚举、注解或者模
块,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_MODULE这七个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。从下图看到,access_flags标志(偏移地址:0x000000EF)的确为0x0021
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合
(interfaces)是一组u2类型的数据的集合
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名
此例中.类索引为ox0003 ,对应为org/fenixsoft/clazz/TestClass 父类索引ox0004, 对应于java/lang/Object接口索引为0
接口索引之后是字段的数量,为ox0001, 表示有一个字段
字段表(field_info)用于描述接口或者类中声明的变量
字段修饰符放在access_flags项目中
此例为ox0002, 即ACC_PRIVATE,即private
跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符
name_index为 ox0005 ,即为常量池中的m
标志描述符
descriptor_index为ox0006 即为常量池中的I, 对应int类型
在descriptor_index之后跟随着一个属性表集合,用于存储一些额外的信息,字段表可以在属性表中附加描述零至多项的额外信息。对于本例中的字段m,它的属性表计数器为0,也就是没有需要额外描述的信息
字段表之后是方法表
方法表结构如下
第一个u2类型的数据(即计数器容量)的值为0x0002,代表集合中有两个方法,这两个方法为编译器添加的实例构造器
之后是属性表
属性表结构如下
属性表长度为ox002f, 即47
Code属性表
Java程序方法体里面的代码经过Javac编译器处理之后,最终变为字节码指令存储在Code属性内
虚拟机读取到字节码区域的长度后,按照顺序依次读入紧随的5个字节,并根据字节码指令表翻译出所对应的字节码指令。翻译“2A B7000A B1”的过程为
1)读入2A,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个变量槽中为reference类型的本地变量推送到操作数栈顶。
2)读入B7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的符号引用。
3)读入000A,这是invokespecial指令的参数,代表一个符号引用,查常量池得0x000A对应的常量为实例构造器“
4)读入B1,查表得0xB1对应的指令为return,含义是从方法的返回,并且返回值为void。这条指
令执行后,当前方法正常结束。
查看javap 结果的字节码指令
{
public org.fenixsoft.clazz.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/fenixsoft/clazz/TestClass;
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lorg/fenixsoft/clazz/TestClass;
}
这个类有两个方法——实例构造器



