概述
一:class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,包含多个数据项目(数据项目的顺序,占用的字节数均由规范定义),各个数据项目严格按照顺序紧凑的排列在Class文件中,不包含任何分隔符,使得整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙。当遇到需要占用超过8位字节以上空间的数据项目时,会按照高位在前的方式分割为多个8位字节进行存储。
class类文件只有两种数据类型:无符号数和表。
无符号数
无符号数属于基本的数据类型,以u1,u2,u4,u8分表代表1个,2个,4个,8个字节。他可以用来描述数字,索引引用,数量值,UTF-8编码的字符串。
表
表是由多个无符号数或者其他表作为数据结构构成的符合数据类型,所有表习惯用“_info”结尾。表用于描述有层次关系的复合数据结构数据,整个class文件其实上就是一张表。
Class文件由于不包含任何分隔符,故表中的数据项,无论是数量还是顺序,都是被严格限定的。哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变
二:魔术和Class文件版本
每个Class文件的头4个字节称为魔数(Magic Number),他唯一作用是用于确定这个文件是否为一个能被虚拟机接受的Class文件。魔数之后的4个字节存储Class的版本号,第5和6字节存储次版本号(minor_version ),第七和第八字节存储主版本号(major_version)。高版本的JDK能向下兼容低版本的JDK,但不能执行更高版本的JDK。
- 原始的java源码
package cn.yuli.jvm;
public class TestClass {
private int m;
public int inc() {
return m+1;
}
}
- 编译成class文件后,通过WinHex打开。
- 魔数(magic)
每个Class文件的头四个字节称为魔数,它的唯一作用是用来确定该文件是否为一个能被虚拟机接受的Class文件。使用魔数而不使用文件扩展名是出于安全方面的考虑,因为文件扩展名可以很随意的被改动
- Class文件版本(minor_version 和 major_version)
minor_version:第四第五字节(从0号开始),次版本号,0x0000
majro_version:第六第七字节(从0号开始),主版本号,0x0034,转化为十进制为52,是使用JDK1.8编译的
三:常量池
常量池可以理解成Class文件中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一。
由于常量池中常量的数目不是固定的,所以在常量池入口首先使用一个2字节长的无符号数constatn_pool_count来代表常量池计数值。
0x0016,转化为十进制为22,即说明常量池中有21个常量(只有常量池的计数是从1开始的,其它集合类型均从0开始),索引值为1~22。第0项常量具有特殊意义,如果某些指向常量池索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可以将索引值置为0来表示。
常量池中主要存放两大类常量:
- 字面量
比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
- 符号引用
属于编译原理方面的概念,包括了下面三类常量:
①.类和接口的全限定名
②.字段的名称和描述符
③.方法的名称和描述符
Java代码在进行Java编译的时候,并不像C和C++那样有”连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过运行期转换的话的话无法得到真正的内存地址,也就是无法被虚拟机使用的。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址之中。
常量池中每一项都是一个表,JDK1.7的时候有14项,每一张表都有自己的结构。但这14种表都有一个共同的特点,第一位是一个u1(一个字节)的标志位(tag),代表当前常量数据哪种常量类型。
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf-8_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_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
首先来看常量池中的第一项常量,其标志位为0x07,是一个CONSTANT_Class_info类型常量,此类型常量代表一个类或接口的符号引用。根据其数据结构,接下来2位字节用来保存一个索引值,它指向常量池中一个CONSTANT_Utf8_info类型的常量,此常量代表了这个类或接口的全限定名,索引值为0x0002,即指向了常量池中的第二项常量。
第二项常量标志位为0x01,确实是一个CONSTANT_Utf8_info类型的常量。根据其数据结构,接下来2个字节用来保存utf-8缩略编码字符串长度,其值为0x000D,转化为十进制为13,即接下来的13个字节为一个utf-8缩略编码的字符串,为com/test/Test,可以看到正好是测试类的全限定名
由于Class文件中,类的全限定名、字段、方法都是使用CONSTANT_Utf8_info类型常量来描述名称,而该常量的长度由2个字节表示,所以类的全限定名、字段名、方法名的最大长度不能超过2个字节所能表示的最大整数,也就是65535。
Class文件可以通过javap的-verbose命令打开,看到具体的内容。
Constant pool:
#1 = Class #2 // cn/yuli/jvm/TestClass
#2 = Utf8 cn/yuli/jvm/TestClass
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Methodref #3.#11 // java/lang/Object."<init>":()V
#11 = NameAndType #7:#8 // "<init>":()V
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcn/yuli/jvm/TestClass;
#16 = Utf8 inc
#17 = Utf8 ()I
#18 = Fieldref #1.#19 // cn/yuli/jvm/TestClass.m:I
#19 = NameAndType #5:#6 // m:I
#20 = Utf8 SourceFile
#21 = Utf8 TestClass.java
- 符号引用
符号引用是一组符号,用来描述所引用的目标,符号是以任何形式存在的字面量。对于符号引用Java虚拟机并没有严格的限制。规定只需要使用的时候能够无歧义定位到目标就可以。常量池存在于Class文件中,而Class文件是必须首先通过Java虚拟机的类加载机制加载到内存中(确切的说是方法区这个内存区域,回顾一下,方法区存放的主要是对象的实例,这个Class文件是虚拟机对外接受访问的接口)。符号引用属于常量池中的内容,那么是不是说符号引用的目标已经加载到内存中了呢?答案是否定的,因为符号引用与虚拟机的内存布局无关,符号引用的目标并不一定已经加载到内存中了。
- 直接引用
直接引用可以是直接指向引用目标的指针、相对偏移量或者是一个能够间接定位到目标的句柄。直接引用是和虚拟机的内存布局有关的,同一个符号引用在不同的虚拟机上翻译的直接引用一般是不同的。如果有了直接引用,那么引用的目标必定是存在内存中的。
四:访问标志
常量池结束之后,是2个字节表示的访问标志(access_flags),这个表示用于识别类或接口层次的访问信息。
譬如:
- 这个Class是类还是接口
- 是否定义public
- 是否定义abstract类型
- 如果是类的话是否被声明为final等
access_flags一共有16个标志位可以使用,当前只定义了其中8个(JDK1.5增加后面3种),没有使用到标志位一律为0。
由于TestClass只是一个被public修饰的普通Java类,并使用JDK1.2之后的编译器进行编译,因此测试类的访问标志为ACC_PUBLIC | ACC_SUPER = 0x0001 | 0x0020 = 0x0021
五:类索引、父类索引和接口索引集合
这三项数据主要用于确定这个类的继承关系。
其中类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引(interface)集合是一组u2类型的数据。
- 类索引(this_class)
用于确定这个类的全限定名,占2字节
- 父类索引(super_class)
用于确定这个类父类的全限定名(Java语言不允许多重继承,故父类索引只有一个。除了java.lang.Object类之外所有类都有父类,故除了java.lang.Object类之外,所有类该字段值都不为0),占2字节
- 接口索引集合(interfaces)
一组u2类型数据的集合。用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果该类本身为接口,则为extends语句)后的接口顺序从左至右排列在接口的索引集合中
this_class、super_class与interfaces按顺序排列在访问标志之后,它们中保存的索引值均指向常量池中一个CONSTANT_Class_info类型的常量,通过这个常量中保存的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串
测试类对应的this_class的值为0x0001,即常量池中第1个常量,super_class的值为0x0003,即常量池中的第3个常量,interfaces_counts的值为0x0000,故接口索引集合大小为0
this_class为: #1 = Class #2 // cn/yuli/jvm/TestClass
super_clas为: #3 = Class #4 // java/lang/Object
六:字段表集合
- 字段表(field_info)
一组字段表类型数据的集合,用于描述接口或类中声明的变量.
- 字段数(fields_count)
:字段表计数器,即字段表集合中的字段表数据个数,占2字节。本测试类其值为0x0001,即只有一个字段表数据,也就是测试类中只包含一个变量(不算方法内部变量)
- 字段(field)
包括类级别(static)和实例级别变量,不包括在方法内部声明的局部变量。
在Java中一般通过如下几项描述一个字段:字段作用域(public、protected、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
字段修饰符,是一个u2类型,用来存放访问标志.
- name_index
代表字段的简单名称,就是去除字段所有类型和修饰的方法或字段,如inc和m,占2字节,是一个对常量池的引用 。本例测试类对应的值为0x0005,即常量池中第5个常量m
- descriptor_index
代表代表参数的描述符,占2个字节,是一个对常量池的引用,其值为0x0006,即常量池中第6个常量I。
#5 = Utf8 m
#6 = Utf8 I
在Java字节码中,类实例表示为”L;”,而void表示为”V”,类似的其他类型也有各自的表示。下表列出了Java字节码中类型表示。
Java 字节码 | 类型 | 描述 |
---|---|---|
B | byte | 单字节 |
C | char | Unicode字符 |
D | double | 双精度浮点数 |
F | float | 单精度浮点数 |
I | int | 整型 |
J | long | 长整型 |
L | 引用 | classname类型的实例 |
S | short | 短整型 |
Z | boolean | 布尔类型 |
[ | 引用 | 一维数组 |
描述符示例
java 代码 | Java 字节码表示 |
---|---|
double d[][][] | [[[D |
java.lang.String toString | ()Ljava/lang/String; |
void inc() | ()V |
- attributes_count
(属性计数器,占2字节。本例的测试类值为0x0000,对应字段m,它的属性表计数器为0,所以该字段没有额外需要描述的信息)
- attributes
属性表集合,后面会讲
字段表集合中不会列出从父类或父接口中继承的字段,但是可能列出原本Java代码之中不存在的字段,如:内部类为了保持对外部类的访问性,自动添加指向外部类实例的字段
Java语言中字段是不能重载的,2个字段无论数据类型、修饰符是否相同,都不能使用相同的名称;但是对于字节码,只要字段描述符不同,字段重名就是合法的。
七:方法表集合
方法表描述的是java文件中存在的方法。方法表和字段表基本一致,只是因为volatile和transient不能修饰方法而少了两个标识符。与此相对,多了synchronized,native,strictfp和abstract关键字修饰符。方法里的代码会被存放到属性表的code属性里。
编译出来的方法表字节码
- methods_count
方法表计数器,即方法表集合中的方法表数据个数。占2字节,其值为0x0002,即测试类中有2个方法
- methods
方法表集合,一组方法表类型数据的集合。方法表结构和字段表结构一样:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
- access_flags(访问标识符)
测试用例中,字节码0x0001为访问标识符,查表得出为ACC_PUBLIC,表示该方法可以被公共访问。
- name_index
代表字段的简单名称,占2字节,是一个对常量池的引用 。本例测试类对应的值为0x0007,即常量池中第7个常量,方法名即为这个常量。说明该方法为编译器自动创建的init方法。
#7 = Utf8 <init>
- descriptor_index
代表代表参数的描述符,占2个字节,是一个对常量池的引用,其值为0x0008,即常量池中第8个常量,说明方法返回的是void类型
#8 = Utf8 ()V
- attribute_count
值为0x0001,说明方法的属性表中有一项属性。
- attribute_info
值为为0x0009,对应常量池为第9项code。(具体的code表分析,会在一下章属性表中解释)
#9 = Utf8 Code
如果父类方法在子类方法中没有被重写,就不会有来自父类方法的信息,但会被编译器自动添加方法,典型的为类构造器clinit和实例构造器init.