Class类文件结构:
- Class文件是一组以8字节为基础单位的二进制流,
- 各个数据项目严格按照顺序紧凑排列在class文件中,
- 中间没有任何分隔符,这使得class文件中存储的内容几乎是全部程序运行的程序。
Java虚拟机规范规定,Class文件格式采用类似C语言结构体的伪结构来存储数据,这种结构只有两种数据类型:无符号数和表。
无符号数
属于基本数据类型,主要可以用来描述数字、索引符号、数量值或者按照UTF-8编码构成的字符串值,大小使用u1、u2、u4、u8分别表示1字节、2字节、4字节和8字节。
表
是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有的表都习惯以“_info”结尾。表主要用于描述有层次关系的复合结构的数据,比如方法、字段。需要注意的是class文件是没有分隔符的,所以每个的二进制数据类型都是严格定义的。具体的顺序定义如下:
魔数
- 每个Class文件的头4个字节称为魔数(Magic Number)
- 唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。
- Class文件魔数的值为0xCAFEBABE。如果一个文件不是以0xCAFEBABE开头,那它就肯定不是Java class文件。
很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如gif或jpeg等在文件头中都存有魔数。使用魔术而不是使用扩展名是基于安全性考虑的——扩展名可以随意被改变!!!
例如:
Class文件的版本号
紧接着魔数的4个字节是Class文件版本号,版本号又分为:
- 次版本号(minor_version): 前2字节用于表示次版本号
- 主版本号(major_version): 后2字节用于表示主版本号。
这个的版本号是随着jdk版本的不同而表示不同的版本范围的。Java的版本号是从45开始的。如果Class文件的版本号超过虚拟机版本,将被拒绝执行。
0X0034(对应十进制的50):JDK1.8
0X0033(对应十进制的50):JDK1.7
0X0032(对应十进制的50):JDK1.6
0X0031(对应十进制的49):JDK1.5
0X0030(对应十进制的48):JDK1.4
0X002F(对应十进制的47):JDK1.3
0X002E(对应十进制的46):JDK1.2
ps:0X表示16进制
即:
常量池
紧接着魔数与版本号之后的是常量池入口.常量池简单理解为class文件的资源从库
- 是Class文件结构中与其它项目关联最多的数据类型
- 是占用Class文件空间最大的数据项目之一
- 是在文件中第一个出现的表类型数据项目
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。
从1开始计数。Class文件结构中只有常量池的容量计数是从1开始的,第0项腾出来满足后面某些指向常量池的索引值的数据在特定情况下需要表达"不引用任何一个常量池项目"的意思,这种情况就可以把索引值置为0来表示(留给JVM自己用的)。但尽管constant_pool列表中没有索引值为0的入口,缺失的这一入口也被constant_pool_count计数在内。例如,当constant_pool中有14项,constant_poo_count的值为15。
常量池之中主要存放两大类常量:
- 字面量: 比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
- 符号引用: 属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
constant_pool_count:占2字节,常量池的计数是从1开始的,其它集合类型均从0开始,索引值为1~n。第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示
constant_pool:表类型数据集合,即常量池中每一项常量都是一个表,共有14种(JDK1.7前只有11种)结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型,常量类型及其数据结构如下表所示:
ps:
这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标识位,取值见上表,代表当前这个常量属于哪种常量类型。
例如:
常量池的计数是从1开始的,这就代表常量池中有28项常量
0A:
CONSTANT_Methodref_info (类中方法的符号引用)
0x0006 #6---指向常量池#6号索引
0x000F #15---指向常量池#15号索引
09:
CONSTANT_Fieldref (对一个字段的符号引用)
0x0010 #16---指向常量池#16号索引
0x0011 #17---指向常量池#17号索引
通过命令即可验证
javap -verbose HelloTest.class
Classfile /Users/liuboyu/Desktop/HelloTest.class
Last modified 2018-9-7; size 426 bytes
MD5 checksum 3820cf8768d7c58bbf1e4242cf82dd18
Compiled from "HelloTest.java"
public class HelloTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#15 // java/lang/Object."":()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 // HelloTest
#6 = Class #22 // java/lang/Object
#7 = Utf8
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 main
#12 = Utf8 ([Ljava/lang/String;)V
#13 = Utf8 SourceFile
#14 = Utf8 HelloTest.java
#15 = NameAndType #7:#8 // "":()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 HelloTest
#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 HelloTest();
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 1: 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 3: 0
line 4: 8
}
SourceFile: "HelloTest.java"
访问标志(2字节)
常量池之后的数据结构是访问标志(access_flags),这个标志主要用于识别一些类或接口层次的访问信息,主要包括:
例如:
查看我们的字节码文件,可以得知
标识位为:ACC_PUBLIC ,ACC_SUPER
验证结果:
类索引、父类索引和接口索引集合
这三项数据主要用于确定这个类的继承关系。
其中类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引(interface)集合是一组u2类型的数据。(多实现单继承)
类索引(this_class)
用于确定这个类的全限定名,占2字节
父类索引(super_class)
用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节
接口索引计数器(interfaces_count)
占2字节。如果该类没有实现任何接口,则该计数器值为0,并且后面的接口的索引集合将不占用任何字节,
接口索引集合(interfaces),一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中
this_class、super_class与interfaces按顺序排列在访问标志之后,它们中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
在我们的例子中:
0x0005:指向常量池 #5
0x0006:指向常量池 #6
0x0000:没有接口
一目了然~
类的全限定名:HelloTest
父类的全限定名:java/lang/Object
字段表集合
fields_count:字段表计数器,即字段表集合中的字段表数据个数,占2字节。
fields:字段表集合,一组字段表类型数据的集合。字段表用于描述接口或类中声明的变量,包括类级别(static)和实例级别变量,不包括在方法内部声明的变量
在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、private修饰符)、是类级别变量还是实例级别变量(static修饰符)、可变性(final修饰符)、并发可见性(volatile修饰符)、可序列化与否(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。在字段表中,变量修饰符使用标志位表示,字段数据类型和字段名称则引用常量池中常量表示。
- access_flags:是一个 u2的数据类型。
- name_index 索引值: 对常量池的引用,代表着字段的简单名称。
- descriptor_index 索引值: 对常量池的引用,代表字段和方法的描述符。
重新写个有字段的类作为例子讲解。
0x0003: fields_count(字段表数量)说明有三个字端表
-
access_flags:0x0009 ---- public static
name_index 索引值: 0x000A ---- 指向常量池#10
descriptor_index 索引值:0x000B --- 指向常量池#11
attributes_count 属性表:0x0000 --- 没有属性表
证明:
则该字段的 name_index 索引值: 0x000A ---- 名字为a
则该字段的 descriptor_index 索引值:0x000B --- I类型,由下图可知,该类型为int类型
-
access_flags:0x0002 ---- private
name_index 索引值: 0x000C ---- 指向常量池#12
descriptor_index 索引值:0x000D --- 指向常量池#13
attributes_count 属性表:0x0000 --- 没有属性表
所以该字段为:
private String str;
- access_flags:0x0001 ---- public
name_index 索引值: 0x000E ---- 指向常量池#14
descriptor_index 索引值:0x000F --- 指向常量池#15
attributes_count 属性表:0x0000 --- 没有属性表
所以该字段为:
public double d
方法表集合
methods_count:方法表计数器,即方法表集合中的方法表数据个数。占2字节
methods:方法表集合,一组方法表类型数据的集合。方法表结构和字段表结构一样。
JVM中堆方法表的描述与字段表是一致的,包括了:访问标志、名称索引、描述符索引、属性表集合。方法表的结构与字段表是一致的,区别在于访问标志的不同。在方法中不能用volatile和transient关键字修饰,所以这两个标志不能用在方法表中。在方法中添加了字段不能使用的访问标志,比如方法可以使用synchronized、native、strictfp、abstract关键字修饰,所以在方法表中就增加了相应的访问标志。
要注意的是,如果父类方法没有在子类中重写,那么在方法中不会自动出现来自父类的方法信息。同样的,有可能添加编译器自动增加的方法,比如方法。
该示例一共有三个方法:init(自带构造器),main,add
方法1: init方法(构造器方法)
0x0001: ACC_PUBLIC
0x0010: 指向常量池#16,查看上面文件可知 #16 = Utf8
0x0011: 指向常量池#17,查看上面文件可知 #17 = Utf8 ()V,为 void
0x0001: 有一个属性
0x0012: 指向常量池#18,查看上面文件可知#18 = Utf8 Code,说明此属性是方法的字节码描述
main,add方法同理,下面我们看一下code属性
属性表集合
在 Class 文件、字段表、方法表都可以携带子机的属性表集合,以用于描述某些场景专有的信息。
与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松了一些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,
Java 虚拟机运行时会忽略掉它不认识的属性。为了能正确解析 Class 文件,《Java 虚拟机规范(第 2 版)》中预定义了 9 项虚拟机实现应当能识别的属性,而在最新的《Java 虚拟机规范(Java SE 7)》版中,
预定义属性已经增加到 21 项,具体内容见表下表
对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说明属性值所占用的位数即可。一个符合规则的属性表应该满足下表中所定义的结构。
Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合中,但并非所有的方法都不许存在这个属性,譬如接口或者抽象类中的方法就不存在Code属性,如果
方法表有Code属性存在,那么它的结构如表所示。
attribute_name_index 是一项指向 CONSTANT_Utf8_info 型常量的索引,常量值固定为“Code”,它代表了该属性的属性名称,attribute_length 指示了属性值的长度,由于属性名称索引与属性长度一共为 6 字节,所以属性值的长度固定为整个属性表长度减去 6 个字节。
max_stack 代表了操作数栈(Operand Stacks)深度的最大值。在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
max_locals 代表了局部变量表所需的存储空间,在这里,max_locals 的单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。对于 byte、char、float、int、short、boolean 和 returnAddress 等长度不超过 32 位的数据类型,每个局部变量占用 1 个 Slot,
而 double 和 long 这两种 64 位的数据类型则需要两个 Slot 来存放。方法参数(包括实例方法中的隐藏参数 “this”)、显式异常处理器的参数(Exception Handler Parameter,就是 try-catch 语句中 catch 块所定义的异常)、方法体中定义的局部变量都需要使用局部变量表来存放。
另外,并不是在方法中用到了多少个局部变量,就把这些局部变量所占 Slot 之和作为 max_locals 的值,原因是局部变量表中的 Slot 可以重写,当代码执行超出一个局部变量的作用域时,这个局部变量所占的 Slot 可以被其他局部变量所使用,
Javac 编译器会根据变量的作用域来分配 Slot 给各个变量使用,然后计算出 max_locals 的大小。
code_length 和 code 用来存储 Java 源程序编译后生成的字节码指令。code_length 代表字节码长度,code 是用于存储字节码指令的一系列字节流。既然叫字节码指令,那么每个指令就是一个 u1 类型的单字节,当虚拟机读取到 code 中的一个字节码时,
就可以对应找出这个字节码代表的是什么指令,并且可以知道到这条指令后面是否需要跟随参数,以及参数应当如何理解。我们知道一个 u1 数据类型的取值范围为 0x00 ~ 0xFF,对应十进制的 0 ~ 255,也就是一共可以表达 256 条指令,目前,Java 虚拟机规范已经定义了其中约 200 条编码值对应的指令含义。
关于 code_length,有一件值得注意的事情,虽然它是一个 u4 类型的长度值,理论上最大值可以达到 2^23-1,但是虚拟机规范中明确限制了一个方法不允许超过65535 条字节码指令,即它实际只使用了 u2 的长度,如果超过这个限制,Javac 编译器也会拒绝编译。一般来讲,编写 Java 代码时只要不是刻意去编写一个超长的方法来为难编译器,是不太可能超过这个最大值的限制。但是,某些特殊情况,例如在编译一个很复杂的 JSP 文件时,某些 JSP 编译会把 JSP 内容和页面输出的信息归并于一个方法之中,就可能因为方法生成字节码超长的原因而导致编译失败。
Code 属性是 Class 文件中最重要的一个属性,如果把一个 Java 程序中的信息分为代码(Code,方法体里面的 Java 代码)和元数据(Metadata,包括类、字段、方法定义及其他信息)量部分,那么在整个 Class 文件中,Code 属性用于描述代码,所有的其他数据项目都用于描述元数据。了解 Code 属性是学习后面关于字节码执行引擎内容的必要基础,能直接阅读字节码也是工作中分析 Java 代码语义问题的必要工具和基本技能,因此笔者准备了一个比较详细的实例来讲解虚拟机是如何使用这个属性的。
继续以上面的代码清单 HelloTest.class 文件为例,如下图所示
前面6个字节(名称索引2字节+属性长度4字节)已经解析过了,所以接下来就是解析剩下的56-6=50字节即可。
max_stack:0x0003 操作数栈的最大深度3
max_locals:0x0001 本地变量表的容量1
code_length:0x0000 000C == 12,字节码区域所占空间的长度12
虚拟机读取到字节码区域的长度后,按照顺序依次读入紧随的 12 个字节,并根据字节码指令表翻译出所对应的字节码指令。翻译“2A B7 00 01 2A 14 00 02 B5 00 04 B1”
code代码,可以通过虚拟机字节码指令进行查找。
Java字节码指令
- 0x2A : aload_0 将引用类型本地变量推送至栈顶
- 0xB7 : invokespecial 调用超类构造方法,实例初始化方法,私有方法
- 0x00 : 什么都不做
- 0x01 : aconst_null 将null推送至栈顶
- 0x2A : aload_0 将引用类型本地变量推送至栈顶
- 0x14 : ldc2_w 将long或do le型常量值从常量池中推送至栈顶(宽索引)
- 0x00 : 什么都不做
- 0x02 : iconst_m1 将int型-1推送至栈顶
- 0xB5 : putfield 为指定的类的实例域赋值
- 0x00 : 什么都不做
- 0x04 : iconst_1 将int型1推送至栈顶
- 0xB1 : return 从当前方法返回void
对照用javap命令计算机字节码指令生成的代码,我们分析的很正确~
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: aload_0
5: ldc2_w #2 // double 1.2d
8: putfield #4 // Field d:D
11: return
LineNumberTable:
line 2: 0
line 5: 4
意外收获:
做android开发的同学可能有遇到过 65535 这个问题,看了jvm发现method_countde 最大值是两个字节的数量也就是0xFFFF,即65535
ps:
本文为小弟本人所理解,如果不对的地方,忘多多指正,共同进步~~~谢谢大家