class 文件结构

class 文件结构

概要

首先我明确一点,如果你希望明白class文件中每个字段的具体内容是什么,最好就别看这篇博客,我主要关注的是整体格式,没有具体研究每个字段中具体的定义和表示。

  1. 什么是class文件,为什么需要class文件
  2. class文件基本结构
  3. class 文件之魔数与class文件的版本
  4. 常量池
  5. 访问标志
  6. 类索引、父类索引与接口索引集合
  7. 字段表集合
  8. 方法表集合
  9. 属性表集合

1. 什么是class文件,为什么需要class文件

首先我们应该明白什么是class文件和我们为什么需要class文件,你才能在头脑中来慢慢理解和消化这个概念。不然上来就介绍基本结构只会导致你很烦这些概念。接下来我们就进入正题。

在我们刚开始学习计算机的时候,老师肯定都和我们说过这样一句话,计算机只认识0和1,因此我们编写出来的程序最终都会编译成0和1这种计算机认识的语言。但是不同的平台又有不同的机器指令。所以如果想让自己的程序在各大主流平台上运行,就必须编译生成对应平台机器指令。

你现在肯定有一个疑问?java是如果做到“一次编写,到处运行”。这就要归功于我们今天要介绍的class文件。其实我们写完java代码之后,如果想运行代码,就必须生成class文件,然后交给JVM,它来帮我们执行。这样只要JVM能把class文件转换成对应平台的机器语言,那么我们写的代码就可以在各大平台上运行。相当于jvm帮我们来又一次将class文件编译生成机器语言。(事实上不是这样的,只是方便理解才这样说的)

那现在可以来回答什么是class文件?其实就是定义在JVM平台上的一种文件。就像浏览器认识HTML这种文件,JVM只认识class这种文件。不过需要注意的一点是,class文件是一个二进制文件,也就是里面只包含0和1。

接下来就好回答为什么需要class文件?简单的说因为JVM只认识class文件,所以我们想要让我的java代码运行,就需要编译生成对应的class文件才能在JVM中运行。

不过这里大家不要产生一个误区只有java代码才能编译生成class文件。任何一种语言都可以编译生成class文件。类如JRuby就是讲ruby代码编译生成class文件的一种编译器。class文件只是和JVM平台绑定,而不是和java语言绑定。

2. class 文件基本结构

class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在class文件之中,中间并没有任何分隔符,这样做的好处是是文件的内容几乎都是必须要使用的内容,减少空间的浪费。根据java虚拟机规范的规定,class文件格式采用一种类似于C语言结构体的伪结构来存储数据。这种伪结构只有俩种数据类型:无符号数和表。后面所有的内容都是围绕着这俩中数据来介绍。

无符号数:其实和我们在代码中使用的无符号数一样,无符号数在class文件中主要来描述数字,索引引用、数量值或者按照UTF-8编码构成字符串值。

表:表是由多个无符号数或者其他表可作为数据项构成的复合数据类型,所有的表都是以_info结尾。表用于描述层次关系的复合结构的数据。其实仔细想想整个class文件就是一张表,这个文件就是按照JVM给定的格式生成一个JVM能认识的表。其实不必把它想的有多深奥,就把它当做是一个按照某个格式生成的文件。

下图就是整个class文件按照先后顺序包含的各个字段:

class 文件结构

从表中可以看出,无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会在其前面使用一个前置的容量计数器来记录其数量,而便跟着若干个连续的数据项,称这一系列连续的某一类型的数据为某一类型的集合,如:fields_count 个 field_info 表数据便组成了方法表集合。这里需要注意的是:Class 文件中各数据项是按照上表的顺序和数量被严格限定的,每个字节代表的含义、长度、先后顺序都不允许改变。

3. 魔数与class文件的版本(对应表中magic,minor_version,major_version)

每个 Class 文件的头 4 个字节称为魔数(magic)(魔数其实就是标注这个文件是什么文件,不同的文件有不同的魔数和文件的后缀名其实一个道理,只是这种相对来说更加安全),它的唯一作用是判断该文件是否为一个能被虚拟机接受的 Class 文件。它的值固定为 0xCAFEBABE。紧接着 magic 的 4 个字节存储的是 Class 文件的次版本号和主版本号,(对应表中的minor_version和major_version)高版本的 JDK 能向下兼容低版本的 Class 文件,但不能运行更高版本的 Class 文件。

4. 常量池(constant_pool_count,constant_pool)

紧接着版本号之后的是常量池,可以理解成class文件之中的资源仓库,也就是当前class文件中需要使用到的一些定义都可以在这里找到,它是 Class 文件中与其他项目关联最多的数据类型,也是占用 Class 文件空间最大的数据项目之一。

常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于 Java 层面的常量概念,如文本字符串、被声明为 final 的常量值等。而符号引用总结起来则包括了下面三类常量:

  • 类和接口的全限定名(即带有包名的 Class 名,如:org.lxh.test.TestClass)
  • 字段的名称和描述符(private、static 等描述符)
  • 方法的名称和描述符(private、static 等描述符)

虚拟机在加载 Class 文件时才会进行动态连接,也就是说,Class 文件中不会保存各个方法和字段的最终内存布局信息,因此,这些字段和方法的符号引用不经过转换是无法直接被虚拟机使用的。当虚拟机运行时,需要从常量池中获得对应的符号引用,再在类加载过程中的解析阶段将其替换为直接引用,并翻译到具体的内存地址中。

这里说明下符号引用和直接引用的区别与关联:

  • 符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到了内存中。
  • 直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那说明引用的目标必定已经存在于内存之中了。

5. 访问标志(access_flag)

在常量池结束之后,紧接着的 2 个字节代表访问标志(access_flag),这个标志用于识别一些类或接口层次的访问信息,包括:这个 Class 是类还是接口,是否定义为 public 类型,abstract 类型,如果是类的话,是否声明为 final,等等。每种访问信息都由一个十六进制的标志值表示,如果同时具有多种访问信息,则得到的标志值为这几种访问信息的标志值的逻辑或。

6. 类索引、父类索引与接口索引集合(this_class、super_class、interfaces)

类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引集合(interfaces)则是一组 u2 类型的数据集合,Class 文件中由这三项数据来确定这个类的继承关系。类索引、父类索引和接口索引集合都按照顺序排列在访问标志之后,类索引和父类索引两个 u2 类型的索引值表示,它们各自指向一个类型为 COMNSTANT_Class_info 的类描述符常量,通过该常量中的索引值找到定义在 COMNSTANT_Utf8_info 类型的常量中的全限定名字符串。而接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是个接口,则应当是 extend 语句)后的接口顺序从左到右排列在接口的索引集合中。

7. 字段表集合(fields_count和field_info)

字段表(field_info)用于描述接口或类中声明的变量。字段包括了类级变量或实例级变量,但不包括在方法内声明的变量。字段的名字、数据类型、修饰符等都是无法固定的,只能引用常量池中的常量来描述。下面是字段表的最种格式:

class 文件结构

其中的 access_flags 与类中的 access_flags类似,是表示数据类型的修饰符,如 public、static、volatile 等。后面的 name_index 和 descriptor_index 都是对常量池的引用,分别代表字段的简单名称及字段和方法的描述符。这里简单解释下“简单名称”、“描述符”和“全限定名”这三种特殊字符串的概念。

前面有所提及,全限定名即指一个事物的完整的名称,如在 org.lxh.test 包下的 TestClass 类的全限定名为:org/lxh/test/TestClass,即把包名中的“.”改为“/”,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“ ;”来表示全限定名结束。简单名称则是指没有类型或参数修饰的方法或字段名称,如果一个类中有这样一个方法 boolean get(int name)和一个变量 private final static int m,则他们的简单名称则分别为 get和 m。

而描述符的作用则是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序等)和返回值的。根据描述符规则,详细的描述符标示字的含义如下表所示:

class 文件结构

对于数组类型,每一维度将使用一个前置的 “[” 字符来描述,如一个整数数组 “int [][]” 将为记录为 “[[I” ,而一个 String 类型的数组 “String[]” 将被记录为 “[Ljava/lang/String” 。

用方法描述符描述方法时,按照先参数后返回值的顺序描述,参数要按照严格的顺序放在一组小括号内,如方法 int getIndex(String name,char[] tgc,int start,int end,char target) 的描述符为 “(Ljava/lang/String[CIIC)I”。

字段表包含的固定数据项目到 descriptor_index 为止就结束了,但是在它之后还紧跟着一个属性表集合用于存储一些额外的信息。比如,如果在类中有如下字段的声明:staticfinalint m = 2;那就可能会存在一项名为ConstantValue 的属性,它指向常量 2。关于 attribute_info 的详细内容,在后面关于属性表的项目中会有详细介绍。

最后需要注意一点:字段表集合中不会列出从父类或接口中继承而来的字段,但有可能列出原本 Java 代码中不存在的字段。比如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

8. 方法表集合(method_count和method_info)

方法表(method_info)的结构与属性表的结构相同,不过多赘述。方法里的 Java 代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Code”的属性里,关于属性表的项目,同样会在后面详细介绍。

与字段表集合相对应,如果父类方法在子类中没有被覆写,方法表集合中就不会出现来自父类的方法信息。但同样,有可能会出现由编译器自动添加的方法,最典型的便是类构造器 “” 方法和实例构造器 “” 方法。

在 Java 语言中,要重载一个方法,除了要与原方法具有相同的简单名称外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此 Java 语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载

9. 属性表集合(attribute_info 和attribute_count)

属性表(attribute_info)在前面已经出现过多系,在 Class 文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。

属性表集合的限制没有那么严格,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,但 Java 虚拟机运行时会忽略掉它不认识的属性。Java 虚拟机规范中预定义了 9 项虚拟机应当能识别的属性(JDK1.5 后又增加了一些新的特性,因此不止下面 9 项,但下面 9 项是最基本也是必要,出现频率最高的),如下表所示:

class 文件结构

对于每个属性,它的名称都需要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,每个属性值的结构是完全可以自定义的,只需说明属性值所占用的位数长度即可。一个符合规则的属性表至少应具有 “attribute_name_info”、“attribute_length” 和至少一项信息属性。

下面主要介绍一个下code属性,至于其他的属性可以参考引用中给的书籍

Code 属性

前面已经说过,Java 程序方法体中的代码讲过 javac 编译后,生成的字节码指令便会存储在 Code 属性中,但并非所有的方法表都必须存在这个属性,比如接口或抽象类中的方法就不存在 Code 属性。如果方法表有 Code 属性存在,那么它的结构将如下表所示:

class 文件结构

attribute_name_index 是一项指向 CONSTANT_Utf8_info 型常量的索引,常量值固定为 “Code”,它代表了该属性的名称。attribute_length 指示了属性值的长度,由于属性名称索引与属性长度一共是 6 个字节,所以属性值的长度固定为整个属性表的长度减去 6 个字节。

max_stack 代表了操作数栈深度的最大值,max_locals 代表了局部变量表所需的存储空间,它的单位是Slot,并不是在方法中用到了多少个局部变量,就把这些局部变量所占 Slot 之和作为 max_locals 的值,原因是局部变量表中的 Slot 可以重用。

code_length 和 code 用来存储 Java 源程序编译后生成的字节码指令。code 用于存储字节码指令的一系列字节流,它是 u1 类型的单字节,因此取值范围为 0x00 到 0xFF,那么一共可以表达 256 条指令,目前,Java 虚拟机规范已经定义了其中 200 条编码值对应的指令含义。code_length 虽然是一个 u4 类型的长度值,理论上可以达到 2^32-1,但是虚拟机规范中限制了一个方法不允许超过 65535 条字节码指令,如果超过了这个限制,Javac 编译器将会拒绝编译。

字节码指令之后是这个方法的显式异常处理表集合(exception_table),它对于 Code 属性来说并不是必须存在的。它的格式如下表所示:

class 文件结构

它包含四个字段,这些字段的含义为:如果字节码从第 start_pc 行到第 end_pc 行之间(不含 end_pc 行)出现了类型为 catch_type 或其子类的异常(catch_type为指向一个 CONSTANT_Class_info 型常量的索引),则转到第 handler_pc 行继续处理,当 catch_pc 的值为 0 时,代表人和的异常情况都要转到 handler_pc 处进行处理。异常表实际上是 Java 代码的一部分,编译器使用异常表而不是简单的跳转命令来实现 Java 异常即 finally 处理机制,也因此,finally 中的内容会在 try 或 catch 中的 return 语句之前执行,并且在 try 或 catch 跳转到 finally 之前,会将其内部需要返回的变量的值复制一份副本到最后一个本地表量表的 Slot中,也因此便有了http://blog.csdn.net/ns_code/article/details/17485221这篇文章中出现的情况。

Code 属性是 Class 文件中最重要的一个属性,如果把一个 Java 程序中的信息分为代码和元数据两部分,那么在整个 Class 文件里,Code 属性用于描述代码,所有的其他数据项目都用于描述元数据。

参考

极客学院

深入理解java虚拟机