Class类文件结构
由于本部分内容概念性知识过多显的过于繁琐,已经尽力精简,且有些细节仍未写到,所以最后以一个反编译文件为例进行类文件结构分析。
文章目录
- Class类文件结构
- 一、Class文件结构
- 魔数与Class文件的版本
- 常量池
- 访问标志
- 类索引、父类索引与接口索引集合
- 字段表集合
- 方法表集合
- 属性表集合
一、Class文件结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,中间没有添加任何分隔符。如果有效文件不是8字节的整数倍则需要补齐为8字节整数倍
Class文件采用一种类似于C语言结构体的伪结构来存储。这种伪结构只有两种数据类型:无符号数和表
- 无符号数:以u1 u2 u4 u8来代表1个字节、2个字节、4个字节、8个字节,可以用来描述数字、索引引用、数量值或者根据UTF8编码构成字符串值
- 表:由多个无符号数或其他表作为数据项构成的复合数据类型,所有表都习惯性以"_info"结尾。
魔数与Class文件的版本
每个Class文件的头4个字节称为魔数,它的唯一作用是用于确定这个文件是否是一个虚拟机可以接受的Class文件,因为只针对后缀名的校验并不安全,可以被随意修改,所以在文件头部增加一个4字节的校验码。
紧接着魔数的4个字节就是存储Class文件的版本号,前两个字节是次版本号(Minor Version),后两个字节是主版本号(Major Version)
Tip:Java的版本号从45开始,JAVA8的主版本号就是52,16进制34换算就是十进制52
常量池
紧接着主版本号之后的就是常量池的入口,常量池是Class文件结构中与其他项目关联最多的数据类型,也是占用空间最大的数据项目之一,同时也是Class文件中第一个出现的表类型数据项目。
由于常量池的数量不固定,与我们写的程序代码有关,所以在常量池入口需要放置一项u2类型的数据,代表常量池数量(计数从1开始)0项常量空出来表示“不引用任何一个常量池项目”。
常量池中主要存放两大类常量:字面量和符号引用。
- 字面量:接近于Java语言层面的常量概念,如String文本字符串、被声明为final的常量值等
- 符号引用:类和接口的全限定名、字段的名称和描述符(public private …)、方法的名称和描述符
Java在进行javac编译时,不像C和C++那样有“连接”步骤,而是在虚拟机加载Class文件时进行动态连接。也就是说Class文件中不保存各个方法和字段的最终内存布局信息,只保存了变量和方法的符号信息,在运行时可以进行动态内存分配加载使用。正是由于这种特性,Java才有了动态扩展的特性。
常量池内的每一项常量都是一个表,共有11中结构各不相同的表结构数据。这11中表都有一个共同的特点,表开始的第一位是一个u1类型的标志位(tag,取值为1-12,2除外)用来表示当前常量的类型。
常量池大小代表其中有19个常量,之后根据tag可以确定每一项常量所占用的长度就可以知道总共常量池的大小。(根据tag也可以对应查找该类型的常量存储格式,篇幅原因,在这里不在展示分析)
访问标志
在常量池结束之后紧接着的2个字节代表访问标志。这个标志用于识别一些类或接口层次的访问信息,包括:这个Class是类还是接口;是否是public类型;是否为abstract类型;是否是final类型…
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令 |
ACC_INTERFACE | 0x0200 | 标识是一个接口 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
注:invokespecial只能调用三类方法:<init>方法;private方法;super.method()
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合(对应JAVA单继承和接口的多实现)。Class文件由这三项数据来确定一个类的继承关系。
对应类查找关系为:
由类索引到常量池中寻找Class信息,又由Class信息找到存储类的全限定名的UTF8字符串。(父类索引、接口索引均类似)
字段表集合
字段表(field_info)用于描述接口或类中声明的变量,不包括方法内部声明的变量,描述的方面可以有:
- 字段的作用域(public private …)
- 是类变量还是实例变量(static)
- 是否可变(final)
- 是否并发可见(volatile)
- 是否可序列化(transient)
- 数据类型(基本类型、数组、对象)
- 字段名称
字段表结构:
类型 | 名称 | 数量 |
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
access_flags描述内容:除了数据类型、字段名称外其余描述方面均由由access_flags描述
name_index:去常量池中寻找字段的简单名称
descriptor_index:去常量池中寻找类型信息找的是描述符
attributes为额外信息(详见属性表集合部分),final常量初始化时attributes里就会存一个ConstantValue的属性为常量赋初始值
tip1:简单名称、描述符、全限定名概念:
全限定名:就是类的全限定名,多个全限定名不产生混淆时,最后会加一个;表示结束
简单名称:则就是没有类型和参数修饰的方法或字段名称
描述符:描述字段的数据类型、方法参数列表和返回值基本类型基本都是首字母大写(byte与boolean首字母重复,所以boolean的描述符为Z)void描述符为V、对象描述符为L、一维数组为[全限定名;、二维数组为[[全限定名;
tip2:在Java语言中字段重载的区别方法不包括返回值,但虚拟机层面可以接受返回值不同的方法重载
方法表集合
方法表的描述与字段表一样,依次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
这些仅用来描述方法的信息,具体方法内部的代码放在属性表集合中一个名为Code的属性里面。
如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但是可能出现由编译器自动添加的方法,例类构造器方法<clinit>方法和实例构造器<init>方法
属性表集合
属性表(attribute_info)集合在之前的表集合中都出现过。与Class文件中其他的数据项目要求严格的顺序、长度和长度不同,属性表不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以想属性表中写入自己定义的属性信息,Java虚拟机会忽略掉它不认识的属性。为了能正确地解析Class文件,Java虚拟机规范预定义了9项虚拟机实现应当能识别的属性。
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java方法代码编译生成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 可序列化 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量表 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成 |
Code属性表:
code表的重要属性:
- max_stack:代表了操作数栈深度的最大值
- max_locals:代表了局部变量表的大小,max_locals的单位为Slot(虚拟机为局部变量分配内存使用的最小单位),对于byte、char等长度不超过32位的数据类型,每个变量占用1个Slot,而double、long这种数据类型需要两个slot存放。方法参数(包括隐藏参数this)、异常处理器的参数、方法体内部定义的局部变量都需要存放在局部变量表中。局部变量表可以复用,当一个变量超出某个变量的作用域后该变量所占用的Slot就可以被复用
- exception_table:异常表,具体后面的例子会说
十六进制的字节码文件知道怎么看就可以,必要时可以参考JAVA虚拟机规范的结构表对照着可以看懂就行,重点是反编译后的文件如何看,下面以一个样例程序查看:
package JVMTest;
public class Test {
private int value1;
public static int value2;
public static final int value3 = 123;
public static final Object obj = new Object();
public static void main(String[] args) {
try {
System.out.println("Hello World");
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("finally-------");
}
}
public void test(){
System.out.println("Test Method");
}
}
本例包括普通变量、静态变量、静态常量、引用类型常量、静态方法、普通方法、异常代码块,比较全面,我们来反编译下看看源码
//类文件位置
Classfile /D:/people/JavaSourceLearn/src/JVMTest/Test.class
//最后修改时间和文件大小
Last modified 2020-6-3; size 913 bytes
//MD5唯一序列号
MD5 checksum b32b11fbe2bb030697c4cd3da6174a0a
//编译自哪个文件
Compiled from "Test.java"
//类信息:public JVMTest包下的Test类
public class JVMTest.Test
//次版本号
minor version: 0
//主版本号,52代表JAVA8
major version: 52
//访问标志位public类型 super允许使用invokespecial指令
flags: ACC_PUBLIC, ACC_SUPER
//常量池内容
Constant pool:
#1 = Methodref #9.#33 // java/lang/Object."<init>":()V
#2 = Fieldref #34.#35 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #36 // Hello World
#4 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = String #39 // finally-------
#6 = Class #40 // java/lang/Exception
#7 = Methodref #6.#41 // java/lang/Exception.printStackTrace:()V
#8 = String #42 // Test Method
#9 = Class #43 // java/lang/Object
#10 = Fieldref #11.#44 // JVMTest/Test.obj:Ljava/lang/Object;
#11 = Class #45 // JVMTest/Test
#12 = Utf8 value1
#13 = Utf8 I
#14 = Utf8 value2
#15 = Utf8 value3
#16 = Utf8 ConstantValue
#17 = Integer 123
#18 = Utf8 obj
#19 = Utf8 Ljava/lang/Object;
#20 = Utf8 <init>
#21 = Utf8 ()V
#22 = Utf8 Code
#23 = Utf8 LineNumberTable
#24 = Utf8 main
#25 = Utf8 ([Ljava/lang/String;)V
#26 = Utf8 StackMapTable
#27 = Class #40 // java/lang/Exception
#28 = Class #46 // java/lang/Throwable
#29 = Utf8 test
#30 = Utf8 <clinit>
#31 = Utf8 SourceFile
#32 = Utf8 Test.java
#33 = NameAndType #20:#21 // "<init>":()V
#34 = Class #47 // java/lang/System
#35 = NameAndType #48:#49 // out:Ljava/io/PrintStream;
#36 = Utf8 Hello World
#37 = Class #50 // java/io/PrintStream
#38 = NameAndType #51:#52 // println:(Ljava/lang/String;)V
#39 = Utf8 finally-------
#40 = Utf8 java/lang/Exception
#41 = NameAndType #53:#21 // printStackTrace:()V
#42 = Utf8 Test Method
#43 = Utf8 java/lang/Object
#44 = NameAndType #18:#19 // obj:Ljava/lang/Object;
#45 = Utf8 JVMTest/Test
#46 = Utf8 java/lang/Throwable
#47 = Utf8 java/lang/System
#48 = Utf8 out
#49 = Utf8 Ljava/io/PrintStream;
#50 = Utf8 java/io/PrintStream
#51 = Utf8 println
#52 = Utf8 (Ljava/lang/String;)V
#53 = Utf8 printStackTrace
//类中具体代码内容
{
//变量value2
public static int value2;
//I表示是int类型
descriptor: I
//表示是public和static类型的
flags: ACC_PUBLIC, ACC_STATIC
//value3常量
public static final int value3;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
//初始化,ConstantValue属性初始化为123
ConstantValue: int 123
//obj 引用类型常量,可以看出没有ConstantValue初始化属性
public static final java.lang.Object obj;
descriptor: Ljava/lang/Object;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
//执行类的构造方法
public JVMTest.Test();
//()表示无参数、V表示返回值类型为void
descriptor: ()V
flags: ACC_PUBLIC
//code表示方法体内部信息
Code:
//stack:操作数栈深度、locals局部变量表大小、args_size参数个数(默认有一个参数this)
stack=1, locals=1, args_size=1
//将局部变量表的0号位置读入操作数栈
0: aload_0
//执行<init>,实例构造方法
1: invokespecial #1 // Method java/lang/Object."<init>":()V
//结束,return 空
4: return
//Java代码行数与字节码指令行号的对应关系
LineNumberTable:
line 3: 0
//执行main方法
public static void main(java.lang.String[]);
//参数为[全限定名;是String类型的一维数组,返回值为void
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
//args_size为1表示有一个参数,因为是static类型,所有没有this参数
stack=2, locals=3, args_size=1
//获取一个静态变量out,类型为L全限定名;PrintStream对象类型
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
//从运行时常量池中获取Hello World字符串
3: ldc #3 // String Hello World
//调用println方法
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//无异常执行final方法
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: ldc #5 // String finally-------
13: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//goto跳转到46行return 结束
16: goto 46
//如果有异常进入catch方法,Exception类型的e变量放入局部变量表后再获取局部变量表1位置中的e变量
19: astore_1
20: aload_1
//执行
21: invokevirtual #7 // Method java/lang/Exception.printStackTrace:()V
//有异常捕获成功执行final方法
24: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
27: ldc #5 // String finally-------
29: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
32: goto 46
//如果出现无法捕获的异常,把catch any放入slot的2号位置(隐藏槽位)
35: astore_2
36: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
39: ldc #5 // String finally-------
41: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
//读取slot2号位置的变量any
44: aload_2
//抛出异常
45: athrow
46: return
//异常信息表
Exception table:
//from 监控起始行号、to监控结束行号,[from,to)、target捕获后跳转行数、捕获类型
from to target type
0 8 19 Class java/lang/Exception
0 8 35 any
19 24 35 any
LineNumberTable:
line 10: 0
line 14: 8
line 15: 16
line 11: 19
line 12: 20
line 14: 24
line 15: 32
line 14: 35
line 15: 44
line 16: 46
//局部变量表
LocalVariableTable:
Start Length Slot Name Signature
20 4 1 e Ljava/lang/Exception;
0 47 0 args [Ljava/lang/String;
//栈图,为加快Class文件的校验速度,把类型校验时需要用到的相关信息直接写入Class文件中
StackMapTable: number_of_entries = 3
frame_type = 83 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 79 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
frame_type = 10 /* same */
public void test();
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 #8 // String Test Method
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 18: 0
line 19: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this LJVMTest/Test;
//静态代码块与静态变量的初始化
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=2, locals=0, args_size=0
0: new #9 // class java/lang/Object
3: dup
//调用Object的<init>方法
4: invokespecial #1 // Method java/lang/Object."<init>":()V
//让obj引用引用Object对象
7: putstatic #10 // Field obj:Ljava/lang/Object;
10: return
LineNumberTable:
line 7: 0
}
//源码文件
SourceFile: "Test.java"