概述
一开始,程序源代码要编译成平台能够理解的机器码才能执行,后来人们开始把源代码编译成能被虚拟机理解的字节码,而虚拟机可以运行在不同的平台上,达到“一次编写,到处运行”的目的。不光是平台无关性,还有语言无关性,虚拟机+字节码,JVM不止能够运行JAVA代码,只要你能编译成JVM认识的class文件,任何语言都可以在JVM上运行, 所以这个JVM的志向相当的远大。
Class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,当遇到需要占用8位以上的数据项时,会按照高位在前的方式分割成若干个8位字节来存储。
Class文件的结构中有两种数据类型:无符号数和表;
- 无符号数是基本数据类型,以u1、u2、u3、u4、u8来表示一个字节、两个字节、四个字节和八个字节的无符号数,无符号数可以表示数字、索引引用、数量值或者以UTF8格式编码的字符串。
- 表是由多个无符号数或者其它表作为数据项构成的复合数据结构,一般以“_info”结尾。用于描述有层次关系的复合结构的数据。
整个Class文件本质上就是一张表,由以下数据项构成:
类型 | 名称 | 数量 |
---|---|---|
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 |
这张表初看很多很复杂,但是当你了解了每一项代表了什么的时候就会发现这张表非常简明,基本上描述出来了一个Class文件里面的数据结构。显然,接下来就要一项一项的看这些都是什么了……
- magic:一个大小为u4的magic段,这是用来标记这是一个Class文件的标记段,只要虚拟机看到一个文件的前4个字节是这个就说明这有可能是一个Class文件,它的内容是:“0xCAFEBABE”?。
- minor_version和major_version,大小都为u2的版本段各一个,应该是JDK1.1之后每升一次大版本号,major_version就加1,JDk1.8的major_version是0x34也就是52,那么1.7就是0x33,51。
- constant_pool_count和costant_pool,大小为u2的常量池计数器(表示有几个常量,从1开始算起,例如0x0016表示22,既有1~21,共21个常量。如果有数据项的常量池引用里面写的是第0个常量,表示不引用常量,其它的集合引用索引都是从0开始的,比如接口索引集合,字段表集合,方法表集合等的容量计数)和常量池。
常量池之中主要存放两大类常量,字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念(如文本字符串、被声明为final的常量值等),而符号引用则属于编译原理方面的概念,包括:类和接口的全限定名;字段的名称和描述符;方法的名称和描述符;
常量池中每一项都是一个表,共有11种结构不同的表结构,这11种结构都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(名为tag,取值为1~12,缺2),这个标记位表明了这个常量属于哪种类型,具体含义如下表:
类型 | 标志 | 描述 |
---|---|---|
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_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
表中所示的每一个类型的常量都单独有自己的结构。如果看到一个常量数据项,先看第一位u1类型的tag,得到它的常量类型,再按照该常量类型的结构读取后面的数据。可以按照Class文件的格式自己人肉读取class文件的内容,也可以使用javap工具解析class文件。至于具体每种类型的结构可以百度一下,太多了我就不打了……
- 按照那个表格,常量池完了就轮到类的访问标志:access_flags了。这是一个u2类型的数据项,用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话是否声明为final,具体含义如下:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial指令,JDK1.2之后编译出来的类的这个标志为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于类和接口来说,此标志为真,其它类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
没有使用到的标志位一律为0,这个数据项可以说明一个类的基本信息。
- 类索引、父类索引与接口索引集合,类索引(this_class),父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据。Class文件由这三项数据确定一个这个类的继承关系,显然,只有Object类的父类索引为0,接口索引集合描述了这个类实现的所有接口,按照implements之后(如果是接口就是extends)的顺序排列。
类索引和父类索引是一个u2类型的索引值,指向常量池里面一个CONSTANT_Class_info类型的常量,这个类型的常量又指向另一个CONSTANT_Utf8_info类型的字符常量,最后在字符串常量中存着类的全限定名。
对于接口索引集合,第一个u2类型是接口索引计数器,表示了后面跟着几个接口索引,如果为0表示没有实现接口。 - 字段表集合,字段表(field_info)用于描述接口或类中声明的变量。字段表包含了类级变量、实例级变量,但是不包括方法中定义的变量。这样的字段可以包含的信息包括:字段的作用域(public、private、protected)、类级还是实例级(static)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。这些信息,除了数据类型和名称都可以用“是否包含”这种标志位来表示,而字段名称和数据类型需引用常量池中的常量。字段表的格式如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attribute | attributes_count |
字段的修饰符放在access_flags中,与类的access_flags类似,字段的标志位含义如下:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_PRIVATE | 0x0002 | 字段是否private |
ACC_PROTECTED | 0x0004 | 字段是否protected |
ACC_STATIC | 0x0008 | 字段是否static |
ACC_FINAL | 0x0010 | 字段是否final |
ACC_VOLATILE | 0x0040 | 字段是否volatile |
ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否enum |
access_flags之后是name_index(简单名称)和descriptor_index(字段和方法的描述符),这两个都是对常量池的引用。在字段表中descriptor_index是字段描述符的索引,也就是这个字段的数据类型,基本数据类型和void都用一个大写的字母表示,对象类型用“L”加上对象的全限定名来表示,数组类型,每一个维度用一个前置的“[”字符来描述,例如:“java/lang/String[][]”被表示成:“[[Ljava/lang/String;”,“int[]”被表示成:“[I”。详细信息如下:
标识字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 特殊类型void |
L | 对象类型,如:Ljava/lang/Object |
当描述符用来描述方法的时候,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一对“()”中,例如:
- void inc()的描述符是:“()V”.
- java/lang/String.toString()的描述符为:“()Ljava/lang/String;”
- int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCouont, int fromIndex)的描述符为:“([CII[CIII)I”
字段表的最后一部分内容就是属性表,会存储字段相关的一些额外信息,详细内容将在后面的属性表小节中讲述。字段表中不会列出从父类或者接口中继承而来的字段,但是有可能列出代码中没有的字段,比如在内部类的字段表中为了保持对外部类的访问会自动添加指向外部类实例的字段。
在Java语言中,字段不能重载,字段的数据类型和修饰符不论是否相同都必须采用不同的名称,但是在字节码中,如果两个字段的描述符不一致,那么就可以重名。
- 方法表,方法表和字段表的结构是一样的,一开始也是access_flags,只不过代表的含义不同,volatile和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE和ACC_TRANSIENT标志。相应的,synchronized、native、strictfp和abstract关键字可以修饰方法,详情如下:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否public |
ACC_PRIVATE | 0x0002 | 方法是否private |
ACC_PROTECTED | 0x0004 | 方法是否protected |
ACC_STATIC | 0x0008 | 方法是否static |
ACC_FINAL | 0x0010 | 方法是否final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是否由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否native |
ACC_ABSTRACT | 0x0400 | 方法是否abstract |
ACC_STRICT | 0x0800 | 方法是否strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生 |
描述符已经在字段部分说了,凭现在这些信息就可以把一个方法的定义描述出来,而方法当中的代码内容是在方法的属性表中。同样的,如果没有重载的话,方法表中没有父类的方法,但是会有类构造器(clinit)和实例构造器(init)。在Java语言中,重载的时候只关注方法名称和参数列表,但是在class文件中,方法的描述符是包含了返回类型的,如果返回类型不一致也是可以区分两个不同的方法的。
- 属性表集合,在Class文件、字段表、方法表中都可以携带自己的属性表集合,用于描述某些场景的专有信息。属性是可以自定义的(要按照一定的规范),JVM虚拟机会忽略自己不认识的属性。下面是几种Java虚拟机应该能识别的属性:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为Deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令对应的关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
一个属性的基本结构是:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u2 | attribute_length | 1 |
u1 | info | attribute_length |
- Code属性:不是每个方法都有Code属性,接口和抽象类的方法是没有Code的(Jdk8应该已经变了)。Code属性表的结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
expection_info | exception_table | exception_table_length |
u2 | attribute_count | 1 |
attribute_info | attributes | attribute_count |
- max_stack,操作数栈(Operand Stacks)深度的最大值,方法运行的任何时刻操作数栈都不会超过这个值,虚拟机运行时要根据这个值来分配栈帧中的操作栈深度。
- max_locals,代表了局部变量表所需的存储空间,单位是Slot,这是虚拟机为局部变量分配内存的所使用的最小单位,对于byte、char、float、int、short、boolean、reference和returnAddress等长度不超过32位的数据类型,每个局部变量占1个Slot,对于double和long两个64位的数据类型则需要用2个Slot来保存。方法参数(包括实例方法的隐藏参数this)、显示异常处理器的参数(try-catch块中catch定义的异常)、方法体中定义的局部变量都要存入局部变量表。并不是用到了多少个局部变量就把所有的Slot加起来就好了,局部变量是有作用域的,编译器会根据作用域计算一番得出max_locals
- code_length和code用来存储编译之后生成的字节码。u4长度的code_length表示后面有几条指令,每个u1长度的字节都算一条指令,显然,Java虚拟机里面最多可以有255条指令,现在已经用了200多条了。(这个code_length的大小是u4,但是虚拟机规范中限制了一个方法最多的指令数是65535,如果超过这个指令数javac就拒绝编译,一般而言不特意写超长的方法不会出问题,只有在复杂的JSP文件中有可能会出现编译失败的问题)。
用来描述指令的code属性是非常非常重要的,书上还说要做到人肉阅读字节码文件来分析Java代码语义问题。让我来看看这一部分内容要不要现在就写在笔记里,要是现在没必要的话有可能放在后面的章节讲解
- 异常表,异常表的结构如下,含义是:如果字节码从第start_pc行到第end_pc行之间(不包含end_pc行)出现了类型为catch_type或其子类的异常则转到handler_pc行处理,当catch_type为0时,代表任何的异常情况都要转向handler_pc进行处理。
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u4 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
以上,code这个属性就讲完了,下面轮到和code平级的其它属性了!!!!!!!!
- Exceptions属性:这个属性列举了方法中可能抛出的受查异常,也就是方法描述里面throws后面列举的异常,结构如下:里面的exception_index_table指向常量池中一个CONSTANT_Class_info类型的常量,代表了受查异常的类型。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
- LineNumberTable属性:用于描述Java源码行号和字节码行号(字节码偏移量)之间的对应关系,不是运行时必须的属性,可以选择不生成,后果是抛异常的时候堆栈不显示行号,调试时也无法按照源码调试。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | line_number_table_length | 1 |
line_number_info | line_number_table | line_number_table_length |
line_number_table,是一个长度为line_number_table_length的类型为line_number_info的集合。line_number_info集合包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。
- LocalVariableTable属性:用于描述栈帧中的局部变量表和Java源码中定义的变量之间的关系,也不是运行时必须的属性,结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | local_variable_table_length | 1 |
local_variable_info | local_variable_table | local_variable_table_length |
具体的对应关系是存在local_variable_info项中,结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | length | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | index | 1 |
还有两段说明,我就直接抄书了。上表中的index是这个局部变量在栈帧局部变量表中Slot的位置。当这个变量的数据类型是64位类型的时候,它占用的Slot是index和index+1。
引入泛型之后,增加了一个姐妹属性,LocalVariableTypeTable,新增的属性和这个属性很像,只是把descriptor_index换成了字段的特征签名,对于非泛型类型来说,字段描述符和特征签名能描述的信息基本是一致的,但是泛型的描述符中的参数化类型被拆除掉了,不能准确的描述泛型,所以出现了新属性。
- SourceFile属性:记录生成这个class文件的源码文件的名称,一般而言类名和文件名一致,但是有一些特殊情况不是(内部类)。结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | source_file_index | 1 |
- ConstantValue属性:用来通知虚拟机自动为静态变量赋值。变量有类级和实例级(有无static)之分,只有类级变量才有可能用到此属性赋值,实例级变量是在实例构造器init方法中进行的。类级变量如果是被final修饰的基本数据类型或字符串类型,那么Sun公司的Javac编译器就会为这个字段生成此属性用来赋值,否则就在类构造器cinit中初始化。
Java虚拟机规范并没有强制要求字段设置ACC_FINAL标志位,要求了只要有这个属性就必须设置ACC_STATIC标志位,这个属性只能用于基本数据类型和String类型。属性结构如下,其中的constantvalue_index是一个常量池的引用。
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | constantvalue_index | 1 |
- InnerClasses属性:用来记录内部类和宿主类之间的关联。如果一个类定义了内部类,那么编译器将会为它及它所包含的内部类生成InnerClass属性,结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_classes | 1 |
inner_classes_info | inner_classes | number_of_classes |
(干,这些乱七八糟的属性感觉没什么意义,简写一下得了,fuck,写完才发现就简写了一个属性?)
- Deprecated及Synthetic属性,这两个属性都属于标志类型的布尔属性,只存在有没有的区别,没有属性值的说法。Deprecated属性用于表示某个类、字段或方法已经不在推荐;Synthetic属性代表不是Java源码中的类、字段和方法,JDK1.5之后也可以设置ACC_SYNCTHETIC标志位来表示,所有非源码产生的都要有它们两种方式的其中一种标识出这是非用户代码产生的,唯一例外是实例构造器init和类构造器cinit。
本章小结
本章详细讲解了Class文件结构中各个组成部分,以及每个部分的定义,数据结构和使用方法。第七章将以动态的、运行时的角度看看字节码流在虚拟机执行引擎中是怎样被解释执行的。