类文件结构
Class文件格式只有两种数据类型:
- 无符号数:属于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数;
- 表:表是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾
| 类型 | 名称 | 数量 |
|---|---|---|
| U4 | magic(魔术) | 1 |
| U2 | minor_version(次版本号) | 1 |
| U2 | major_version(主版本号) | 1 |
| U2 | constant_pool_count(常量个数) | 1 |
| cp_info | constant_pool(常量池表) | constant_pool_count-1 |
| U2 | access_flags(类的访问控制权限) | 1 |
| U2 | this_class(类名) | 1 |
| U2 | super_class(父类名) | 1 |
| U2 | interfaces_count(接口个数) | 1 |
| U2 | interfaces(接口名) | interfaces_count |
| U2 | fields_count(字段个数或者叫变量个数) | 1 |
| field_info | fields(变量表) | fields_count |
| U2 | methods_count(方法个数) | 1 |
| method_info | methods(方法表) | methods_count |
| U2 | attributes_count(附加属性个数) | 1 |
| attribute_info | attributes(附加属性表) | attributes_count |
注意:常量池中的容量计数是从1开始的而不是从0开始,也就是说,常量池中常量的个数是这个容器计数-1。将0空出来的目的是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。
class文件中只有常量池的容量计数是从1开始的,对于其它集合类型,比如接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始的。
HelloWorld示例:
/**
* @author yangdong
* @date 2021-01-07
*/
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world");
}
}
在命令行下使用javac编译就可以得到class文件,如下:
CA FE BA BE 00 00 00 34 00 1D 0A 00 06 00 0F 09 00 10 00 11 08 00 12 0A 00 13 00 14 07 00 15 07
00 16 01 00 06 3C 69 6E 69 74 3E 01 00 03 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E 65 4E
75 6D 62 65 72 54 61 62 6C 65 01 00 04 6D 61 69 6E 01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67
2F 53 74 72 69 6E 67 3B 29 56 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 01 00 0F 48 65 6C 6C 6F 57
6F 72 6C 64 2E 6A 61 76 61 0C 00 07 00 08 07 00 17 0C 00 18 00 19 01 00 0B 68 65 6C 6C 6F 20 77
6F 72 6C 64 07 00 1A 0C 00 1B 00 1C 01 00 1B 63 6E 2F 69 74 63 61 73 74 2F 6A 76 6D 2F 74 32 2F
48 65 6C 6C 6F 57 6F 72 6C 64 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 01 00 10
6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D 01 00 03 6F 75 74 01 00 15 4C 6A 61 76 61 2F 69
6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B 01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74
72 65 61 6D 01 00 07 70 72 69 6E 74 6C 6E 01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72
69 6E 67 3B 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1D
00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 07 00 09 00
0B 00 0C 00 01 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2 00 02 12 03 B6 00 04 B1 00 00 00 01
00 0A 00 00 00 0A 00 02 00 00 00 09 00 08 00 0A 00 01 00 0D 00 00 00 02 00 0E
魔数与Class文件的版本
CA FE BA BE 00 00 00 34
class文件的头4个字节称为魔数,它的唯一作用就是确定这个文件时候是一个能被虚拟机接受的class文件。很多图片格式都用一个魔数来标识文件类型,比如png和jpg等。在java的class文件中,这个数是0xcafebabe。
接下来就是class文件的版本号,第5、6个字节是次版本号,第7、8个字节是主版本号。在这里,次版本号是0,主版本号是52,(十六进制是34) 表示是 Java 8。Java的版本号是从45开始的,JDK1.1之后的每一个JDK大版本主版本号向上加1,高版本的JDK能向下兼容低版本的JDK。
常量池
常量池中主要存放两大类常量:
- 字面量:比如说int a = 1; 这个1就是字面量。又比如String a = "abc",这个abc就是字面量
- 符号引用:一个java类将会编译成一个class文件。在编译时,java类并不知道引用类的实际内存地址,因此只能使用符号引用来代替。比如org.simple.People
常量池中的每一项都是一个表,在JDK1.7之前有11中结构不同的表结构,在JDK1.7中为了更好的支持动态语言调用,又增加了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info)。不过这里不会介绍这三种表数据结构
| 类型 | 标志 | 含义 |
|---|---|---|
| CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
| CONSTANT_Integer_info | 3 | 整型字面量 |
| CONSTANT_Float_info | 4 | 浮点型字面量 |
| CONSTANT_Long_info | 5 | 长整形字面量 |
| CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
| CONSTANT_Class_info | 7 | 类或接口的符号引用 |
| CONSTANT_String_info | 8 | 字符串类型字面量 |
| CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
| CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
| CONSTANT_InterfaceMethod_info | 11 | 接口中方法的符号引用 |
| CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
| CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
| CONSTANT_MethodType_info | 16 | 标识方法类型 |
| CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
由class文件结构图可知,常量池的开头两个字节0x001D是常量池的容量计数,这里是29,也就是说,这个常量池中有28个常量项。
看看这个例子的第一项,容量计数后面的第一个字节标识这个常量的类型,是0A,即10,查表可知是类方法的符号引用;并且类描述符CONSTANT_Class_info的索引项是6(0x0006),名称及类型描述符CONSTANT_NameAndType_info的索引项是15(0x000F)。
接下来的tag是9,可知是一个字段的符号引用,看图可以知道类或接口描述符CONSTANT_Class_info的索引项是16(0x0010),字段描述符CONSTANT_NameAndType_info的索引项是17(0x0011)。
再接下来的tag是8,可知是一个字符串类型的字面量,看图可以知道字符串字面量的索引是18(0x0012)。
一直按如上方法推理即可将全部常量推理出来,这里就不一一推理了;
查看类文件全部指令信息
查看HelloWorld.class的全部指令信息:
//查看类文件全部指令信息命令
javap -v HelloWorld
//输出
Classfile /F:/BaiduNetdiskDownload/资料 解密JVM/代码/jvm/src/cn/itcast/jvm/t2/HelloWorld.class
Last modified 2021-1-7; size 442 bytes
MD5 checksum 53024b0dae4997bfca798749538c7cda
Compiled from "HelloWorld.java"
public class cn.itcast.jvm.t2.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream;
#3 = String #18 // hello world
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V
#5 = Class #21 // cn/itcast/jvm/t2/HelloWorld
#6 = Class #22 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream;
#18 = Utf8 hello world
#19 = Class #26 // java/io/PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V
#21 = Utf8 cn/itcast/jvm/t2/HelloWorld
#22 = Utf8 java/lang/Object
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
{
public cn.itcast.jvm.t2.HelloWorld();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String hello world
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 9: 0
line 10: 8
}
SourceFile: "HelloWorld.java"
结合上面我们推理的内容来看,首先是一个方法符号引用,内容是6与15。表示它引用了常量池中 #6 和 #15 项,然后看看索引是#6的常量,是一个Utf8编码的字符串,内容是java/lang/Object。
然后看看索引是#15的常量,是一个NameAndType类型,这个常量的内容是#7:#8,索引是#7的内容是< init > ,索引是#8的内容是()V,这表示了一个方法的名称、参数类型和返回类型,具体的含义在后面的方法表中介绍。这样,这个方法的内容就是java/lang/Object.< init >()V。
访问标志
常量池结束后紧接着的两个字节代表访问标志,用来标识一些类或接口的访问信息,包括:这个Class是类还是接口;是否定义为public;是否定义为abstract;如果是类的话,是否被声明为final等。具体的标志位以及含义如下表:
由于access_flags是两个字节大小,一共有三十二个标志位可以使用,当前仅仅定义了8个,没有用到的标志位都是0。
对于一个类来说,可能会有多个访问标志,这时就可以对照上表中的标志值取或运算的值。拿上面那个例子来说,它的访问标志值是0x0021,查表可知,这是ACC_PUBLIC和ACC_SUPER值取或运算的结果。所以HelloWorld这个类的访问标志就是ACC_PUBLIC和ACC_SUPER;
类索引、父类索引和接口索引集合
在访问标志access_flags后接下来就是类索引(this_class)和父类索引(super_class),这两个数据都是u2类型的;
由于Java中是单继承,所以父类索引只有一个;但Java类可以实现多个接口,所以接口索引是一个集合。因此class文件由这三个数据项来确定类的继承关系!
类索引用来确定这个类的全限定名,这个全限定名就是说一个类的类名包含所有的包名,然后使用"/"代替"."。比如Object的全限定名是java.lang.Object。父类索引确定这个类的父类的全限定名,除了Object之外,所有的类都有父类,所以除了Object之外所有类的父类索引都不为0.接口索引集合存储了implements语句后面按照从左到右的顺序的接口。
类索引和父类索引都是一个索引,这个索引指向常量池中的CONSTANT_Class_info类型的常量。然后再CONSTANT_Class_info常量中的索引就可以找到常量池中类型为CONSTANT_Utf8_info的常量,而这个常量保存着类的全限定名;
以上面的例子来说,this_class的值是0x0005,即十进制的5,super_class的值是0x0006,即十进制的6;根据指令信息来看,推理是对的。
由于这个类没有实现接口,所以接口索引集合的容量计数是0。如果容量计数是0,就不需要存储接口的信息;
字段表集合
字段表用来描述接口或类中声明的变量(不包括方法内部声明的变量),描述的信息包括:
- 字段的作用域(public,protected,private修饰)
- 是类级变量(static修饰)还是实例变量
- 是否可变(final修饰)
- 并发可见性(volatile修饰)
- 是否可序列化(transient修饰)
- 字段数据类型(8种基本数据类型,对象,数组等引用类型)
- 字段名称
前面5个修饰符,都是布尔值,用标志位来表示;后面两个字段名称和类型,是无法固定的,只能引用常量池中的常量来表示。
access_flags 是一个 u2 类型,表示各种修饰符;
注意:字段访问标志跟上面的类或接口访问标志不一样
由于这个类没有定义字段,所以fields_count为0;
方法表集合
Class 文件存储格式中对方法的描述和字段的描述基本上是一致的。也是依次包括:
- 访问标志(access_flags)
- 名称索引(name_index)
- 描述符索引(descriptor_index)
- 属性表集合数量(attributes_count)
- 属性表集合(attributes)
方法表的结构也和字段表相同,这里就不再列出。不过,方法表的访问标志和字段的不同,如下图:
根据二进制文件查看,methods_count为0x0002,即一共有2个方法,访问标志为1(0x0001)即为public,然后名称索引为7(0x0007)对应常量池< init >,描述符索引为8(0x0008)对应常量池()V。然后属性表集合数量为1(0x0001)表示属性的个数为1,然后看属性表结构(见属性表集合章节),属性名称索引为9(0x0009)对应常量池Code属性。属性长度索引为29(0x00 00001D),然后后面就是具体的属性了;
属性表集合
| 类型 | 名称 | 数量 |
|---|---|---|
| U2 | attribute_name_index | 1 |
| U4 | attribute_length | 1 |
| U1 | info | attribute_length |
从上表可以看出,class文件规定的属性格式只有前6个字节:两个字节的属性名称的索引和4个字节的属性长度;
Code属性的结构如下:
| 类型 | 名称 | 数量 |
|---|---|---|
| U2 | attribute_name_index | 1 |
| U4 | attribute_lenght | 1 |
| U2 | max_stack | 1 |
| U2 | max_locals | 1 |
| U4 | code_length | 1 |
| U1 | code | code_length |
| U2 | exception_table_length | 1 |
| exception_info | exception_table | exception_table_length |
| U2 | attributes_count | 1 |
| attribute_info | attributes | attributes_count |
- max_stack:操作数栈的最大深度,在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机执行时需要根据这个值来分配栈帧中的操作栈深度;
- max_locals:局部变量表最大槽(slot)数;
- code_length:字节码指令个数
- code:字节码指令
SourceFile属性
对应class二进制数据:
SourceFile属性记录生成这个class文件的源码文件名称。在上面的数据中,0001表示属性表集合中有一个属性,0x000D(即十进制13)是属性名的索引值,查找常量池可以知道是SourceFile,0x00000002是这个属性的长度,即两个字节,最后的两个字节就是这个属性的内容,是一个常量池索引,0x000E,十进制14,结果是HelloWorld.java。
还有很多常用的属性,不过由于篇幅关系就不列出来了。
参考原文:blog.csdn.net/u012877472/… www.cnblogs.com/ysocean/p/1…