3.1 Class类文件结构
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符。根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:“无符号数”和“表”。无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-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 |
3.1.1 魔数与Class文件版本
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。
3.1.2 常量池
紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的,如常量池容量(偏移地址:0x00000008)为十六进制数0x0016,即十进制的22,这就代表常量池中有21项常量,索引值范围为1~21。在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:
- 被模块导出或者开放的包(Package)
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
- 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)
常量池中每一项常量都是一个表,最初常量表中共有11种结构各不相同的表结构数据,后来为了更好地支持动态语言调用,额外增加了4种动态语言相关的常量,所以截至JDK13,常量表中分别有17种不同类型的常量。这17类表都有一个共同的特点,表结构起始的第一位是个u1类型的标志位(tag,取值见表中标志列)),代表着当前常量属于哪种常量类型。
| 类型 | 标志 | 描述 |
|---|---|---|
| CONSTANT Uf8 info | 1 | UTF-8编码的字符串 |
| CONSTANT Integer info | 3 | 整型字面量 |
| CONSTANT Float info | 4 | 浮点型字面量 |
| CONSTANT Long _info | 5 | 长整型字面量 |
| CONSTANTDouble 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 | 字段或方法的部分符号引用 |
| CONSTANTMethodHandle info | 15 | 表示方法句柄 |
| CONSTANTMethodType _info | 16 | 表示方法类型 |
| CONSTANT_ Dynamic_info | 17 | 表示一个动态计算常量 |
| CONSTANT InvokeDynamic info | 18 | 表示一个动态方法调用点 |
| CONSTANT Module_ info | 19 | 表示一个模块 |
| CONSTANT Package jinfo | 20 | 表示一个模块中开放或者导出的包 |
3.1.3 访问标志
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;等等。
| 标志名称 | 标志值 | 含义 |
|---|---|---|
| ACC _PUBLIC | 0x0001 | 是否为public类型 |
| ACC_ FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
| ACC_ SUPER | 0x0020 | 是否允许使用invokespecial 字节码指令的新语义,invokespecial 指令的语义在JDK 1.0.2 发生过改变,为了区别这条指令使用哪种语义,JDK1.0.2之后编译出来的类的这个标志都必须为真 |
| ACC_ INTERFACE | 0x0200 | 标识这是一个接口 |
| ACC_ABSTRACT | 0x0400 | 是否为abstract 类型,对于接口或者抽象类来说,此标志值为真,其他类型值为假. |
| ACC_ SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
| ACC_ ANNOTATION | 0x2000 | 标识这是一个注解 |
| ACC_ ENUM | 0x4000 | 标识这是一-个枚举 |
| ACC_ MODULE | 0x8000 | 标识这是一个模块 |
access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一律为零。TestClass是一个普通Java类,不是接口、枚举、注解或者模块,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、ACC_ENUM、ACC_MODULE这七个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。
3.1.4 类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合。
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
对于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。
3.1.5 字段表集合
字段表(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 | attributes | attributes_count |
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型,其中可以设置的标志位和含义如下:
| 标志名称 | 标志值 | 含义 |
|---|---|---|
| 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。它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。
- 全限定名仅仅是把类全名中的“.”替换成了“/”而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”号表示全限定名结束。
- 简单名称则就是指没有类型和参数修饰的方法或者字段名称,这个类中的inc()方法和m字段的简单名称分别就是“inc”和“m”。
- 描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,对于数组类型,每一维度将使用一个前置的“[”字符来描述。用描述符来描述方法时,按照先参数列表、后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符为“([CII[CIII)I”。 | 标识字符 | 含义 | 标识字符 | 含义 | | —- | —- | —- | —- | | B | 基本类型byte | J | 基本类型long | | C | 基本类型char | S | 基本类型short | | D | 基本类型double | Z | 基本类型boolean | | F | 基本类型float | V ⊝ | 特殊类型void | | I | 基本类型int | L | 对象类型,如Ljava/lang/Object; |
字段表的第一个u2类型的数据为容量计数器fields_count。字段表所包含的固定数据项目到descriptor_index为止就全部结束了,不过在descrip-tor_index之后跟随着一个属性表集合,用于存储一些额外的信息,如果将字段m的声明改为“final static int m=123;”,那就可能会存在一项名称为ConstantValue的属性,其值指向常量123。
字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段。
3.1.6 方法表集合
Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项,如表所示。这些数据项目的含义也与字段表中的非常类似,仅在访问标志和属性表集合的可选项中有所区别。
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | access_flags | 1 |
| u2 | name_index | 1 |
| u2 | descriptor_index | 1 |
| u2 | attributes_count | 1 |
| attribute_info | attributes | attributes_count |
因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对,synchronized、native、strictfp和abstract关键字可以修饰方法,方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。
方法里的Java代码,经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目。方法表集合的入口地址为0x00000101,第一个u2类型的数据(即计数器容量)的值为0x0002,代表集合中有两个方法。
与字段表集合相对应地,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样地,有可能会出现由编译器自动添加的方法,最常见的便是类构造器“
3.1.7 属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。为了能正确解析Class文件,《Java虚拟机规范》最初只预定义了9项所有Java虚拟机实现都应当能识别的属性,而在最新的《Java虚拟机规范》的Java SE 12版本中,预定义属性已经增加到29项。
| 属性名称 | 使用位置 | 含义 |
|---|---|---|
| Code | 方法表 | Java代码编译成的字节码指令 |
| ConstantValue | 字段表 | 由final 关键字定义的常量值 |
| Deprecated | 类,方法表、字段表 | 被声明为deprecated的方法和字段 |
| Exceptions | 方法表 | 方法抛出的异常列表 |
| Enclosing Method | 类文件 | 仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标示这个类所在的外围方法 |
| InnerClasses | 类文件 | 内部类列表 |
| LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
| LocalVariableTable | Code属性 | 方法的局部变量描述 |
| StackMapTable | Code属性 | JDK6中新增的属性,供新的类型检查验证器(TypeChecker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
| Signature | 类、方法表、字段表 | JDK 5中新增的属性,用于支持范型情况下的方法签名。在Java语言中,任何类,接口,初始化方法或成员的泛型签名如果包含了类型变量(TypeVariables)或参数化类型( Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的范型采用擦除法实现,为了避免类型信息被擦除后导致签名混乱,需要这个属性记录范型中的相关信息 |
| SourceFile | 类文件 | 记录源文件名称 |
| SourceDebugExtension | 类文件 | JDK5中新增的属性,用于存储额外的调试信息。譬如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR 45提案为这些非Java语言编写,却需要编译成字节码并运行在Java虚拟机中的程序提供了一个进行调试的标准机制,使用该属性就可以用于存储这个标准所新加入的调试信息 |
| Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
| LocalVariableTypeTable | 类 | JDK 5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
| Runtime VisibleAnnotations | 类,方法表、字段表 | JDK 5中新增的属性,为动态注解提供支持。该属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的 |
| RuntimeVisible Annotations | 类、方法表、字段表 | JDK 5中新增的属性,与RuntimeVisibleAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的 |
| RuntimeVisibleParameterAnnotations | 方法表 | JDK5中新增的属性,作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法参数 |
| RuntimeInvisibleParameterAnnotations | 方法表 | JDK 5中新增的属性,作用与RuntimelnvisibleAnnotations属性类似,只不过作用对象为方法参数 |
| AnnotationDefault | 方法表 | JDK 5中新增的属性,用于记录注解类元素的默认值 |
| BootstrapMethods | 类文件 | JDK 7中新增的属性,用于保存invokedynamie指令引用的引导方法限定符 |
| RuntimeVisible TypeAnnotations | 类,方法表、字段表,Code属性 | JDK 8中新增的属性,为实现JSR 308中新增的类型注解提供的支持,用于指明哪些类注解是运行时(实际上运行时就是进行反射调用)可见的 |
| RuntimeInvisibleType Annotations | 类、方法表、字段表,Code属性 | JDK8中新增的属性,为实现JSR308中新增的类型注解 提供的支持,与RuntimeVisibleTypeAnnotations属性作用刚好相反,用于指明哪些注解是运行时不可见的 |
| MethodParameters | 方法表 | JDK8中新增的属性,用于支持(编译时加上parameters参数)将方法名称编译进Class文件中,并可运行时获取。此前要获取方法名称(典型的如IDE的代码提示)只能通过JavaDoc中得到 |
| Module | 类 | JDK9中新增的属性,用于记录一个Module的名称以及相关信息( requires,exports,opens,,uses,provides ) |
| ModulePackages | 类 | JDK 9中新增的属性,用于记录一个模块中所有被exports 或者opens的包 |
| ModuleMainClass | 类 | JDK 9中新增的属性,用于指定-一个模块的主类 |
| NestHost | 类 | JDK 11中新增的属性,用于支持嵌套类(Java中的内部类)的反射和访问控制的API,一个内部类通过该属性得知自己的宿主类 |
| NestMembers | 类 | JDK 11中新增的属性,用于支持嵌套类( Java中内部类)的反射和访问控制的API,一个宿主类通过该属性得知自己有哪些内部类 |
对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。
| 类型 | 名称 | 数量 |
|---|---|---|
| u2 | attribute name index | 1 |
| u4 | attribute_ length | 1 |
| u1 | info | attribute_ length |
3.2 字节码指令简介
在Java虚拟机的指令集中,大多数指令都包含其操作所对应的数据类型信息。还有另外一些指令,则是与数据类型无关的指令。有一些单独的指令可以在必要的时候用来将一些不支持的类型转换为可被支持的类型。大部分指令都没有支持整数类型byte、char和short,甚至没有任何指令支持boolean类型。编译器会在编译期或运行期将byte和short类型的数据带符号扩展(Sign-Extend)为相应的int类型数据,将boolean和char类型数据零位扩展(Zero-Extend)为相应的int类型数据。
