【JVM】类文件结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目按照顺序紧凑地排列在Class文件中,中间没有任何分隔符。
使用命令javac将.java 文件编译为.class文件
使用命令javap输出.class文件的字节码内容
Class文件格式采用类似于C语言结构体的伪结构,只有两种数据类型:无符号数和表。
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
-
表:由多个无符号数或其他表作为数据项构成的复合数据类型。所有表都以
_info
结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表,由下表中的数据项构成。
名称 | 类型 | 数量 |
---|---|---|
magic | u4 | 1 |
minor_version | u2 | 1 |
major_version | u2 | 1 |
constant_pool_count | u2 | 1 |
constant_pool | cp_info | constant_pool_count - 1 |
access_flags | u2 | 1 |
this_class | u2 | 1 |
super_class | u2 | 1 |
interfaces_count | u2 | 1 |
interfaces | u2 | interfaces_count |
fields_count | u2 | 1 |
fields | field_info | fields_count |
methods_count | u2 | 1 |
methods | method_info | methods_count |
attributes_count | u2 | 1 |
attributes | attribute_info | attributes_count |
学习类文件结构,就是要明白上表中各个数据项的具体含义。
魔数与Class文件版本
-
魔数
-
即上表中的
magic
,4个字节,在Class文件的开头。 -
作用是确定该文件是否为一个被虚拟机接受的Class文件。使用魔数而不是后缀名的方式是基于安全的考虑:文件扩展名可以任意更改。
-
正常的Class文件的魔数为
Java.png0xCAFEBABE
。cafe babe
,咖啡宝贝。
-
版本号
-
即上表中的
major_version
、minor_version
:Class文件的第5、6个字节是次版本号minor_version
,第7、8个字节是主版本号major_version
。 -
高版本的JDK可以向下兼容,但低版本的JDK不向上兼容,虚拟机会拒绝执行。
常量池
常量池是Class文件中的资源仓库,它是Class文件结构中与其他数据项关联最多的数据类型,也是占用Class文件空间最大的数据项之一。
-
常量池容器计数值
-
即上表中的
constant_pool_count
,2个字节 -
常量池中的常量的数量是不固定的,所以需要
constant_pool_count
来表示常量池中常量的数量。 -
该容器计数从1开始(而不是Java语言习惯中从0开始),比如
constant_pool_count
的值为16,则说明常量池中有15项常量,索引值范围为1-15。将第0项常量空出来的目的是为了满足后面某些指向常量池的索引值的数据在特定情况下表达“不引用任何一个常量池项”的含义。 -
常量池
-
从概念上理解,
常量池 = constant_pool[constant_pool_count - 1]
。 -
常量池中主要存放两大类常量:字面量和符号引用。
①字面量:接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。
②符号引用:属于编译原理方面的概念,主要包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。Java代码在经过javac命令编译之后,生成的Class文件中并不会保存各个方法、字段的最终内存布局信息,必须等到虚拟机运行时,从常量池中获得对应的符号引用,再通过这些符号引用经过类创建或运行时解析、翻译等才能得到具体的内存地址。 -
常量池中共有14中类型的常量,每一个常量(项)都有自己的表结构,但这14种表的有一个共同特点:表开始的第一位是一个u1类型的标志位tag,代表这个常量属于哪种常量类型。常量池中的常量类型见下表。
常量类型 | 表类型 | tag取值 |
---|---|---|
UTF-8编码的字符串 | CANSTANT_Utf8_info | 1 |
整型字面量 | CANSTANT_Integer_info | 3 |
浮点型字面量 | CANSTANT_Float_info | 4 |
长整型字面量 | CANSTANT_Long_info | 5 |
双精度浮点型字面量 | CANSTANT_Double_info | 6 |
类或接口的符号引用 | CANSTANT_Class_info | 7 |
字符串类型字面量 | CANSTANT_String_info | 8 |
字段的符号引用 | CANSTANT_Fieldref_info | 9 |
类中方法的符号引用 | CANSTANT_Methodref_info | 10 |
接口中方法的符号引用 | CANSTANT_IntefaceMethodref_info | 11 |
字段或方法的部分符号引用 | CANSTANT_NameAndType_info | 12 |
方法句柄 | CANSTANT_MethodHandle_info | 15 |
方法类型 | CANSTANT_MethodType_info | 16 |
动态方法调用点 | CANSTANT_InvokeDynamic_info | 18 |
访问标志
即Class文件数据项表中的access_flags
,用于识别类或者接口层次的访问信息:该是类还是接口,是否为public,是否定义为abstract;如果是类,是否声明为final;该类是否由用户代码产生,是否为注解,是否为枚举等信息。
类索引、父类索引和接口索引
Class文件根据这三项数据来确定这个类的继承关系。
-
类索引
即Class文件数据项表中的this_class
:用于确定这个类的全限定名。 -
父索引
即Class文件数据项表中的super_class
:用于确定这个类的父类的全限定名。由于Java是单继承,只有一个父类。所有类(除了java.lang.Object
)都有父类。 -
接口索引
即Class文件数据项表中的interfaces_count
和interfaces
。interfaces_count
表示接口索引表的容量,如果该值为0,接口索引表不占用任何字节。
其中,类索引和父索引各自指向一个常量池中的类型为CANSTANT_Class_info
的常量,通过该常量的索引值再定位到常量池中的CANSTANT_Utf8_info
类型的常量表示的全限定名字符串。
字段表集合
从概念上理解,字段表集合 = fields[fields_count]
,用于描述该Class文件对应的代码中声明的变量:包括类级变量以及实例变量,但不包括方法内声明的局部变量。
对于一个字段,描述信息主要有:作用域(public、private、protected),是实例变量还是类变量(有无static修饰),可变性(final),并发可见性(volatile),可否被序列化(transient),数据类型(基本类型、对象、数组),名称。这些信息中各个修饰符都是布尔值(要么有,要么没有),用标志位表示,而名称、数据类型则引用常量池中的常量来描述。
每一个字段会对应一个字段表,字段表的最终结构如下。
名称 | 类型 | 数量 |
---|---|---|
access_flag | u2 | 1 |
name_index | u2 | 1 |
descriptor_index | u2 | 1 |
attributes_count | u2 | 1 |
attributes | attribute_info | attributes_count |
-
access_flag
该字段的修饰符信息:是否public、private、protected、static、final、volatile、transient、enum等。 -
name_index
该字段的简单名称,是对常量池的常量引用。比如在代码中定义private String name
,则name字段的简单名称就是name
,但是这个name
这个字面量是在常量池中的,name_index
存储的是对常量池中该常量项的引用。 -
descriptor_index
该字段的描述符,描述字段的数据类型。 -
attributes和attributes
该字段的属性表,见下文。
字段表集合中不会列出从超类或父接口中继承而来的字段,但有可能会列出原本Java代码中不存在的字段,比如在内部类中为了保持对外类的访问性,在编译Class文件的时候会自动添加外部类的实例字段。
方法表集合
从概念上理解,方法表集合 = methods[methods_count]
。每一个方法对应一个方法表method_info
。方法表与字段表的结构一致,只是具体的信息项不同。
名称 | 类型 | 数量 |
---|---|---|
access_flag | u2 | 1 |
name_index | u2 | 1 |
descriptor_index | u2 | 1 |
attributes_count | u2 | 1 |
attributes | attribute_info | attributes_count |
其中,访问信息access_flag
包括是否public、private、protected、static、final、synchronized、native、abstract、strictfp、是否接受不定参数、是否由编译器自动产生等。
其中,方法描述符descriptor_index
中描述了方法的参数列表(数量、类型、顺序)和返回值。
而方法体中的代码,经过编译器编译成字节码指令后,存储在方法属性表attributes[attributes_count]
中。
如果子类没有重写父类的方法,则子类的方法表集合中不会出现来自父类的方法信息,但可能会出现编译器自动添加的方法,如类构造器<clinit>
方法和实例构造器<init>
方法。
属性表集合
在Class文件、字段表、方法表中都可以包含自己的属性表集合,以此描述某些场景专有的信息。
1、Code属性
Java程序方法体中的代码经过javac编译器编译之后,最终变为字节码指令存储在Code属性表中,即Code属性表是方法表的一部分。但并非所有的方法表都存在该属性,比如接口或者抽象类中的抽象方法就不存在Code属性表。Code属性表的结构如下。
名称 | 类型 | 数量 |
---|---|---|
attribute_name_index | u2 | 1 |
attribute_length | u4 | 1 |
max_stack | u2 | 1 |
max_locals | u2 | 1 |
code_length | u4 | 1 |
code | u1 | code_length |
exception_table_length | u2 | 1 |
exception_table | exception_info | exception_length |
attributes_count | u2 | 1 |
attributes | attribute_info | attributes_count |
-
attribute_name_index和attribute_length
attribute_name_index
表示该属性表的名称,即Code,是指向常量池中类型为CANSTANT_Utf8_info
的常量的索引。
attribute_length
表示该属性值的长度。 -
max_stack
代表了操作数栈深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配帧栈中的操作深度。 -
max_locals
代表了局部变量表所需要的存储空间。max_locals
的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对于byte、char、float、int、short、boolean、returnAddress等长度不超过32位的数据类型,每个局部变量占用一个Slot,double和long这两种64位的数据类型则需要两个Slot。方法参数(包括实例方法中的隐藏参数this)、显式异常处理器的参数(try-catch中catch块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。局部变量表中的Slot可以重用,当代码执行超过一个局部变量的作用域时,这个局部变量所占用的Slot可以被其他局部变量所使用。Javac编译器会根据变量的作用域来分配Slot给各个变量使用,然后计算出max_locals的大小。 -
code_length和code
-
code_length
代表字节码长度,即字节码指令的个数,也就是code的长度。虽然是u4类型(2^32),但虚拟机规范中明确规定一个方法不允许超过65535条字节码指令,即它只使用了u2的长度,超过这个长度,Javac编译器会拒绝编译。 -
code
中存储的是字节码指令的一系列字节流。对于字节码指令,每个指令都是单字节(u1类型)。当虚拟机读取到code中的一个字节码时,就可以找出对应的这个字节码对应的指令,并且可以知道这个指令后面是否需要跟随参数以及参数应当如何理解。因为字节码指令是用1个字节(8位,2^8=256)来表示,所以一共可以表示256条指令。目前,Java虚拟机规范已经定义了其中约200条编码值对应的指令含义。 -
exception_table_length和exception_table
显式异常处理表集合,对于Code属性来说并不是必须的,表示的是try-catch中的异常信息描述。
Code属性是Class文件中最重要的一个属性,如果把一个Java程序的信息分为代码和元数据两部分,那么在整个Class文件中,Code属性用于描述代码,所有的其他数据项目都用于描述元数据。
2、Exceptions属性
属于方法表中的一部分。作用是列出出方法中可能抛出的受检查异常,也就是方法描述时在throws 关键字后面列出的异常。
3、LineNumberTable属性
属于Code属性的一部分。用于描述Java源代码行号与字节码行号之间的对应关系,它并不是运行时必须的属性,默认生成,可以在javac命令中使用 -g:none
或-g:lines
属性取消生成该信息。不生成该信息对程序运行的影响:当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。
4、LocalVariableTable属性
属于Code属性的一部分。用于描述帧栈中局部变量表中的变量与Java源代码定义的变量之间的关系,不是运行时必须的属性,默认生成,可以在javac命令中使用 -g:none
或-g:vars
属性取消生成该信息。不生成该信息的影响:当其他人引用该方法时,所有的参数名称都会丢失,IDE会使用类似arg0、arg1等占位符代替原有的参数名,给代码编写带来不便。
5、ConstantValue属性
属于字段表中的一部分。作用是通知虚拟机自动为静态变量赋值,只有被static关键字修饰的变量(类变量)才可以使用该属性。虚拟机对类变量和实例变量的赋值方式和时机有所不同。对于实例变量的赋值是在实例构造器<init>方法中进行的。对于类变量,如果是同时有static和final修饰的基本类型数据或String类型数据,则会生成ConstantValue属性来进行初始化,否则在<clinit>方法中进行初始化。
实例分析
定义一个父类Animal,两个接口Eat、Sleep,一个要分析的Rabbit类。
package constructor;
public class Animal {
protected String weight;
public String getWeight() {
return weight;
}
public void setWeight(String weight) {
this.weight = weight;
}
}
package constructor;
public interface Sleep {
void sleep();
}
package constructor;
public interface Eat {
void eat();
}
package constructor;
public class Rabbit extends Animal implements Eat, Sleep{
private String nickName;
private int age;
public static final boolean isCute = true;
@Override
public void eat() {
System.out.println("I eat grass");
}
@Override
public void sleep() {
System.out.println("I sleep well");
}
public String play(int temperature){
if(temperature > 10){
return "I want to play outside";
}else {
return "I want to stay at home";
}
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
使用命令javac *.java
编译所有源文件,生成class文件。(如果仅仅单独编译Rabbit.java文件会提示找不到类Sleep、Eat和Animal等)。
使用javap -verbose Rabbit.class
命令查看Rabbit.class
文件的字节码内容。
Classfile /Users/yue/Documents/workspace/idea/datacenter/src/test/constructor/Rabbit.class
Last modified 2017-7-9; size 1092 bytes
MD5 checksum 64e6283bb3c70e9c41fb2f72e09aae13
Compiled from "Rabbit.java"
public class constructor.Rabbit extends constructor.Animal implements constructor.Eat,constructor.Sleep
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #11.#41 // constructor/Animal."<init>":()V
#2 = Fieldref #42.#43 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #44 // I eat grass
#4 = Methodref #45.#46 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #47 // I sleep well
#6 = String #48 // I want to play outside
#7 = String #49 // I want to stay at home
#8 = Fieldref #10.#50 // constructor/Rabbit.nickName:Ljava/lang/String;
#9 = Fieldref #10.#51 // constructor/Rabbit.age:I
#10 = Class #52 // constructor/Rabbit
#11 = Class #53 // constructor/Animal
#12 = Class #54 // constructor/Eat
#13 = Class #55 // constructor/Sleep
#14 = Utf8 nickName
#15 = Utf8 Ljava/lang/String;
#16 = Utf8 age
#17 = Utf8 I
#18 = Utf8 isCute
#19 = Utf8 Z
#20 = Utf8 ConstantValue
#21 = Integer 1
#22 = Utf8 <init>
#23 = Utf8 ()V
#24 = Utf8 Code
#25 = Utf8 LineNumberTable
#26 = Utf8 eat
#27 = Utf8 sleep
#28 = Utf8 play
#29 = Utf8 (I)Ljava/lang/String;
#30 = Utf8 StackMapTable
#31 = Utf8 getNickName
#32 = Utf8 ()Ljava/lang/String;
#33 = Utf8 setNickName
#34 = Utf8 (Ljava/lang/String;)V
#35 = Utf8 getAge
#36 = Utf8 ()I
#37 = Utf8 setAge
#38 = Utf8 (I)V
#39 = Utf8 SourceFile
#40 = Utf8 Rabbit.java
#41 = NameAndType #22:#23 // "<init>":()V
#42 = Class #56 // java/lang/System
#43 = NameAndType #57:#58 // out:Ljava/io/PrintStream;
#44 = Utf8 I eat grass
#45 = Class #59 // java/io/PrintStream
#46 = NameAndType #60:#34 // println:(Ljava/lang/String;)V
#47 = Utf8 I sleep well
#48 = Utf8 I want to play outside
#49 = Utf8 I want to stay at home
#50 = NameAndType #14:#15 // nickName:Ljava/lang/String;
#51 = NameAndType #16:#17 // age:I
#52 = Utf8 constructor/Rabbit
#53 = Utf8 constructor/Animal
#54 = Utf8 constructor/Eat
#55 = Utf8 constructor/Sleep
#56 = Utf8 java/lang/System
#57 = Utf8 out
#58 = Utf8 Ljava/io/PrintStream;
#59 = Utf8 java/io/PrintStream
#60 = Utf8 println
{
public static final boolean isCute;
descriptor: Z
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 1
public constructor.Rabbit();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method constructor/Animal."<init>":()V
4: return
LineNumberTable:
line 11: 0
public void eat();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String I eat grass
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 21: 0
line 22: 8
public void sleep();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String I sleep well
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 26: 0
line 27: 8
public java.lang.String play(int);
descriptor: (I)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: iload_1
1: bipush 10
3: if_icmple 9
6: ldc #6 // String I want to play outside
8: areturn
9: ldc #7 // String I want to stay at home
11: areturn
LineNumberTable:
line 30: 0
line 31: 6
line 33: 9
StackMapTable: number_of_entries = 1
frame_type = 9 /* same */
public java.lang.String getNickName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #8 // Field nickName:Ljava/lang/String;
4: areturn
LineNumberTable:
line 38: 0
public void setNickName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #8 // Field nickName:Ljava/lang/String;
5: return
LineNumberTable:
line 42: 0
line 43: 5
public int getAge();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #9 // Field age:I
4: ireturn
LineNumberTable:
line 46: 0
public void setAge(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #9 // Field age:I
5: return
LineNumberTable:
line 50: 0
line 51: 5
}
SourceFile: "Rabbit.java"
内容摘抄自《深入理解Java虚拟机》