目录
一.概述
在Java中,JVM可以理解的代码就叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的操作系统的计算机上运行。
实现语言无关性的基础仍然是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的任何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集和符号表以及若干辅助信息。基于安全方面的考虑,Java虚拟机规范要求在Class文件中使用许多强制性的语法和结构化约束,但任何一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。
Clojure(Lisp 语言的一种方言)、Groovy、Scala 等语言都是运行在 Java 虚拟机之上。下图展示了不同的语言被不同的编译器编译成.class文件最终运行在 Java 虚拟机之上。.class文件的二进制格式可以使用WinHex查看。
可以说.class文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。
二.Class类文件的结构
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前(最高位字节在地址最低位,最低位字节在地址最高位)的方式分隔若干个8位字节进行存储。
任何一个Class文件都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不不一定都得定义在文件里(譬如类或接口也可以通过类加载器直接生成)。下面讨论只是通俗地将任意一个有效的类或接口所应当满足的格式称为“Class文件格式”,实际上它并不一定以磁盘文件的形式存在。
根据Java虚拟机规范的规定,Class文件格式采用一种类似C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表。
无符号数属于基本的数据类型,以u1,u2,u4,u8分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成的字符串。
表是由多个无符号数或者其他表作为数据项构成的符合数据类型,所有表都习惯性以“_info”结尾,表用来描述有层次关系的复合结构的数据,整个Class文件本质上就是一张表。由下面的数据项构成:
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类会可以有个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
Class文件字节码结构组织示意图:
无论是顺序还是数量,甚至于数据存储的字节序这样的细节,都是被严格规定的,哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
2.1 魔数
u4 magic; //Class 文件的标志
每个Class文件的头4个字节称为魔数(Magic Number),它唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都使用魔数进行身份识别。程序设计者很多时候都喜欢用一些特殊的数字表示固定的文件类型或者其它特殊的含义。
2.2 Class文件的版本
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
紧接着魔数的4个字节存储的是Class文件的版本号:第5个和第6个字节是次版本号(Minor Version),第7个和第8个字节是主版本号(Major Version)。高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行其他版本号的Class文件。
2.3 常量池
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间的最大的数据项目之一,同时它还是在Class文件中第一个出现表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。这个容量计数是从1而不是0开始的,将第0位常量空出来是由特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic Reference)。字面量比较接近Java语言层面的常量概念,如文本字符串,声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三种常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。也就是说,在Class文件中不会保存各个方法,字段的最终内存布局信息,因此这些字段,方法的符号引用不经过运行期转换的话无法得到真正的内存入口地址,也就是无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析,翻译到具体的内存地址之中。
常量池中的每一个常量都是一个表,这些表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位,代表当前这个常量属于哪种常量类型。
类型 | 标志(tag) | 描述 |
---|---|---|
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 | 字段或方法的符号引用 |
CONSTANT_MothodType_info | 16 | 标志方法类型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
2.4 访问标志
在常量池结束之后,紧跟着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型等。具体的标志位以及标志的含义如下:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译出来的类这两个标志都必须为真。 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口 |
ACC_ABSTARCT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类值为假 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
acc_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0。
2.5 类索引,父类索引与接口索引集合
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个雷可以实现多个接口
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用来确定这个类的全限定名,父类索引于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所以的Java类都有父类,因此除了java.lang.Object之外,所以的Java类的父类索引都不为0.接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左向右排列在接口索引集合中。
类索引,父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个CONSTANT_Class_info的类型描述符常量,通过CONSTANT_Class_info类型的常量中的索引值就可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。
对于接口索引集合,入口中的第一项-u2类型的数据为接口计数器,表示索引表的容量。如果该类没有实现任何接口,则该计数器为0,后面的索引表不再占用任何字节。
2.6 字段表集合
u2 fields_count;//Class 文件的字段的个数
field_info fields[fields_count];//一个类会可以有个字段
字段表(filed_info)用于描述接口或者类中的声明的变量。字段包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。
field info(字段表) 的结构:
- access_flags:字段的作用域(public,private,protected修饰符),是实例变量还是类变量(static修饰符),是否被序列化(transient修饰符),可变性(final),可见性(volatile修饰符,是否强制从主内存读写)。
- name_index:对常量池的引用,表示字段的名称。
- descriptor_index:对常量池的引用,表示字段和方法的描述符。
- attributes_count:一个字段还会拥有一些额外的属性,attributes_count存放属性的个数。
- attributes[attributes_count]:存放具体属性具体内容。
上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。
字段access_flags的取值:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_FINAL | 0x0010 | 字段是否final |
ACC_PRIVATE | 0x0002 | 字段是否private |
ACC_PROTECTED | 0x0004 | 字段是否protected |
ACC_STATIC | 0x0008 | 字段是否static |
ACC_VOLATILE | 0x0040 | 字段是否volatile |
ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_ENUM | 0x4000 | 字段是否enum |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生的 |
细节:
descriptor_index描述符的作用是用来描述字段的数据类型,方法的参数列表(包括数量,类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte,char,double,float,int,long,short,boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名表示。对于数组类型,每一维度将使用一个前置的“[”字符来描述。用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。
字段表集合中不会列出从超类或者父接口继承而来的字段,但有可能列出原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型,修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来说,如果两个字段的描述符不一致,字段重名就是合法的。
2.7 方法表集合
u2 methods_count;//Class 文件的方法的数量
method_info methods[methods_count];//一个类可以有个多个方法
方法表的结构依次包含了访问标志,名称索引,描述符索引,属性表集合几项。
方法表中的access_flags取值:
标志名称 | 标志值 | 含义 |
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_SYNCHRONIZED | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是否由编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否由编译器自动产生的 |
与字段集合对应的,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样的,有可能出现由编译器自动添加的方法。
在Java语言中,要重载一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征前面,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法也可以共存。也就是说,如果两个方法具有相同的名称和特征签名,但返回值不同,那么也是可以合法共存同一个Class文件中的。
2.8 属性表集合
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
在Class文件,字段表,方法表都可以携带自己的属性表集合,以用来描述某些场景专有信息。属性表集合的限制稍微宽松了一些,不再要求各个属性表有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。