Java语言从诞生之时就宣称一次编写,到处运行的跨平台特性,其实现原理是源码文件并没有直接编译成机器指令,而是编译成Java虚拟机可以识别和运行的字节码文件(Class类文件,*.class),字节码文件是一种平台无关的中间编译结果,字节码文件由java虚拟机读取,解析和执行,java虚拟机屏蔽了不同操作系统和硬件平台的差异性。
如今的java虚拟机已经称为一种通用平台,不但能够运行java语言,Groovy,JRuby,Jython等一大批动态语言也可以直接在Java虚拟机上运行,其原理也是这些动态语言的编译器将源码文件编译为和Java相同的字节码文件,这样Java虚拟机就可以像执行java语言一样执行这些动态语言了。
字节码class类文件是由一系列字节码命令组成,用于表示程序中各种常量、变量、关键字和运算符号的语义等等。Java的Class类文件是一组以8为字节为单位的二进制流,各个数据项严格按照顺序紧凑地排列在Class类文件之中,中间没有添加任何分隔符,当遇到需要占用8位字节以上空间的数据项时,按照高位在前的方式分割成若干个8位字节进行存储。
Java虚拟机规定,Class类文件格式采用类似C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表:
(1).无符号数:
属于基本类型的数据,以u1, u2, u4, u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码的字符串值。
(2).表:
由多个无符号数或其他表作为数据项构成的复合数据类型,所以表都习惯性地以“_info“结尾。表用于描述有层次关系的复合结构数据,整个Class文件本质就是一张表。
Java 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类文件没有任何分隔符,是严格按照这个结构表顺序排列,下面具体介绍各个名称含义:
(1).magic(魔数):
每个Class文件的头4个字节被称为魔数,它的唯一作用是用于确定这个文件是否为一个能被java虚拟机所接收的Class类文件,即用于判定文件是否是符合规范的java Class文件。虽然说后缀名“.class”可以表明文件是一个Class文件,但是文件后缀名是可以随意被改动的,基于安全的考虑,很多文件都通过魔数值来唯一确定文件类型,java的Class文件魔数是:0xCAFEBABE.
(2).minor_version和major_version(主次版本号):
每个Class文件的第5和第6个字节代表Class文件的次版本号,第7和第8个字节代表Class文件的主版本号。
Class文件的主、次版本号是由JDK决定的,JDK1.0~JDK1.1使用了45.0~45.3的版本号(45是主版本好,点”.“之后的是次版本号),从JDK1.1开始,每个大版本的JDK主版本号加1.
Class主、次版本号的一个作用是,高版本的Java虚拟机可以向前兼容,运行低版本JDK编译的Class字节码文件,而低版本的java虚拟机不能运行高版本JDK编译的Class字节码文件。当低版本的java虚拟机运行高版本JDK编译的Class字节码文件时,通常会报类似如下的异常:
- Exception in thread "main" java.lang.UnsupportedClassVersionError: a (Unsupporte
- d major.minor version 49.0)
JDK1.0~JDK1.1使用了45.0~45.3的版本号,JDK1.2使用了46.0~46.65535的版本号,JDK1.3使用了47.0~47.65535的版本号,JDK1.4使用了48.0~48.65535的版本号,JDK1.5使用了49.0~49.65535的版本号,JDK1.6使用了50.0~50.65535的版本号,JDK1.7使用51.0~51.65535的版本号。
在编译时可以通过指定-target参数来改变主版本号,如JDK1.6编译时如果没有给定target参数,则编译出来的Class文件的主版本号是50,如果给定”-target 1.4 -source 1.4”参数之后,则主版本将变为48,如果给定”-target 1.5 ”参数之后,则主版本将变为49。
(3). constant_pool_count和constant_pool:
constant_pool_count代表Class文件中常量池的数目,由于常量池的计数从1开始,因此常量池的容量是constant_pool_count-1。
第0项常量空出做特殊考虑,为了满足一些指向常量池的索引值在某些特定情况下需要表达“不指向任何一个常量池”的意思。
constant_pool常量池是Class类文件中出现的第一个表类型数据,常量池主要存放两大类常量:
a.字面量(Literal):包括文本字符串、final类型常量值。
b.符号引用(SymbolicReferences):包括类和接口的全限定名、字段的名称和描述符、方 法的名称和描述符。
(4). access_flags:
用于表示Class或接口层次的访问标志,即类或接口层面的访问控制信息,通常存储的信息包括:Class类文件是类、接口、枚举或是注解;是否定义为public类型;是否定义为abstract类型;类是否被定义为final等等。
(5). this_class、super_class和interfaces:
this_class类索引用于确定类的全限定名,super_class父类索引用于确定父类的全限定名,interfaces接口索引用于确定接口的全限定名,由于java中可以实现多个接口,因此使用interfaces_count来存储接口数量。
(6). field:
field_info字段表用于描述接口或者类中声明的变量,field字段包括了类级变量(静态变量)和实例级变量(成员变量),但不包括方法内部的局部变量。
fields_count字段数目表示Class文件中的类和实例变量总数,字段存放的信息包括:字段访问标志、是否静态、是否final、是否并发可见volatile、是否可序列化transient、数据类型、字段名称等等。
注意:字段表中不包含从父类或者接口中继承而来的字段,但是会添加原本代码中不存在的字段,例如this,以及内部类对外部类访问而自动添加的外部类实例字段等。
(7).method:
method_info方法表用于描述类或者接口中声明的方法,methods_count用于表示Class文件中方法总数,method方法存储了方法的访问标识、是否静态、是否final、是否同步synchronized、是否本地方法native、是否抽象方法abstract、方法返回值类型、方法名称、方法参数列表等信息。
方法的代码指令并没有直接存放在方法表中,而是存放着属性表中的方法表Code中。
注意:如果父类的方法在子类没有被重写,方法表中不会出现来自父类的方法信息,但是编译器会自动添加类构造器”<clinit>”方法和实例构造器”<init>”方法。
Java编译器的方法特征签名只包括:方法名称、参数顺序和参数类型,不包括方法返回值类型,因此java的方法重载不能通过方法的返回值类区别,但是在Class文件中,方法特征签名包括方法的返回值类型,因此Class文件中可以共存两个名称和参数完全相同而返回值类型不同的方法。
(8). attribute:
attribute_info属性表是Class文件格式中最具扩展性的一种数据项目,用于存放field_info字段表、method_info方法表以及Class文件的专有信息,属性表不要求各个属性有严格顺序,只要求不与已有的属性名字重复即可,属性表中存放的常用信息如下:
属性名称 | 使用位置 | 含义 |
Code | 方法表 | Java代码编译后的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为Deprecated的字段或方法 |
Exception | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | java源码行号和字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成 |
魔数
每个class文件的头4个字节称为魔数(Magic Number),其值为:0xCAFEBASE,它的唯一作用是用于确定这个文件是否为一个能被虚拟机接受的class文件。使用魔数而不是扩展名来进行识别主要是基于安全的考虑,因为文件的扩展名可以随意地被改动。
版本号
紧接着魔的4个字节存储的是class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。java的版本是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号上加1(JDK1.0-1.1使用了45.0-45.3的版本号),高版本的JDK能向下兼容以前版本的class文件,但不能运行以后版本的class文件,即使文件格式并未发生变化。JDK1.2对应主版本号为46,JDK1.3为47,依此类推。
常量池
紧接着主次版本号之后的是常量池入口,常量池是class文件结构中与其它项目关联最多的数据类型,也是占用class文件空间最大的数据项目之一,同时它还是class文件中第一个出现的表类型数据项目。
由于常量池中常量的数据是不固定的,所以在常量池的入口需要放置一荐u2类型的数据,代表常量池容量计算值(constant_pool_count)。与Java语言习惯不一样的是,这个容量计数是从1而不是0开始的。将第0项常量出来的目的是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的意思。class文件结构中只有常量池的容量计数是从1开始,对于其它集合类型,包括接口索引集合,字段表集合,方法表集合的容量计算都是从0开始的。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。字面量比较接近于Java语言层面的常量概念,如文本字符串,被声明为final的常量值等。而符号引用则属性编译原理方面的概念,包含了下面三类常量:
a.类和接口的全限定名(Fully Qualified Name)
b.字段的名称和描述符(Descriptor)
c.方法的名称和描述符
常量池中的每一项常量都是一个表,共有11种结构各不相同的表结构数据,这11种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位,代表当前这个常量属性哪种常量类型,
access_flags中一共有32个标志位可以使用,当前只定义了其中的8个,没有使用到的标志位要求一律为0。
类索引(this_class)和父类索引(super_class)都是u2类型的数据,而接口索引(interfaces)是一组u2类型的数据集合,class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因了除了java.lang.Object之外,所有Java类的父类索引都不为0。接口索引集合用来描述这个实现实现了哪些接口,这些被实现的接口将按照implements语句后的接口顺序从左到右排列在接口的索引集合中。
类索引,父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名称字符串。对于接口索引集合,入口的第一项为u2类型的数据,表示接口计数器(interfaces_countD),表示索引表的容量。如果该类没有实现任何接口,那么该计数器值为0,后面接口的索引表不再占用任何字节。
字段表(field_info)用于描述接口或类中声明的变量。字段(field)包括了类级变量或实例变量,但不包括方法内部声明的变量。描述一个字段的信息有:字段的作用域(public,private,protected修饰符),是类级变量还是实例级变量(static修饰符),可变性(final),并发可见性(volatile修饰符,是否强制从主内存读写),是否可序列化(transient修饰符),字段数据类型(基本数据类型,对象,数组),字段名称。这些信息中,各个修改符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字,字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中的常量来描述。下面是字段表的最终格式。
字段修饰符放在access_flags项目中,它与类的access_flags项目是非常相似的,都是一个u2的数据类型,其中可以设置的标志位和含义如下表:
跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称及字段的描述符。现在需要解释一下“简单名称”,“描述符”及前面出现过多次的“全限定名”这三种特殊字符串的概念。
全限制名称和简单名称很好理解,如“org/fenixsoft/clazz/TestClass"就是一个类全限制名,仅仅是把类名中的”.“替换成了”/“而已,为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加上一个“;”号表示全限定名结束。简单名称就是指没有类型和参数修饰的方法或字段名称。
相对于全限定名和简单名称来说,方法和字段的描述符就要复杂一些。描述符的作用是来用描述字段的数据类型,方法的参数列表(包括数量,类型及顺序)和返回值。根据描述符规则,基本数据类型(byte,char,double,float,int,long,short,boolean)及代表无返回值的void类型都使用一个大写字符来表示,而对象类型则用字符L加对象全限定名来表示,如下图:
对于数组类型,每一维度使用一个前置的“[”字符来描述,如一定义为“java.lang.String[][]”类型的二维数组,将被记录为:“[[java/lang/String;”,一个整型数组“int[]”将被记录为“[I”。
用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序在一组小括号“()”之内。如方法void int()描述符为:”()V“,方法java.lang.String toString()描述符为:“()java/lang/String;”
字段表都包含的固定数据项目到descriptor_index为止就结束了,但是在descriptor_index之后跟随着一个属性表集合用于存储一些额外的信息,字段都可以在属性表中描述0至多项额外的信息。字段表集合中不会列出超类或父接口中继承而来的字段,但有可能列表出原来Java代码中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。另外,在Java语言中字段是无法重载的,两个字段的数据类型,修饰符不管是否相同,都必须使用不一样的名称,但是对于字段码来讲,如果两个字段的描述符不一致,那字段重名就是合法的。
因为volatile关键字和transient关键字不能修改方法,所以方法表的访问标志中没有了ACC_VOLATILE与ACC_TRANSIENT标志。与之相对的,synchronized, native, strictfp和abstract关键字可以修饰方法,所以方法表的访问标志中增加了ACC_SYNCHRONIZED,ACC_NATIVE,ACC_STRICTFP,ACC_ABSTRACT标志。对于方法表,所有标志位及取值如下表:
方法里面的Java代码,经过编译器编译成字节码指令后,存放在方法属性表集合中一个名为“Cocde”的属性表中,属性表是class文件桥口中最具扩展性的一种数据项目。
与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现父类的方法。但同样的,可能会出现由编译器自动添加的方法,最典型的便是类构造器“<client>”方法和缺省实例构造器“<init>”方法。在Java语言中,要重(Override)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包在特征签名之中,因为Java语言里是无法仅仅依靠返回值的不同来对一个已有的方法进行重载的。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。
Class文件是二进制文件,使用支持二进制的文本编辑器打开之后显示的全是二进制数据,非常的不便于阅读和理解,使用JDK提供的javap工具可以简单将Class反编译,编译理解Class文件的结构,例子如下:
源码:
- public class Test {
- public int getNum(int i) {
- return i + 1;
- }
- }
javap反编译之后的字节码文件:
- public class Test extends java.lang.Object
- SourceFile: "Test.java"
- minor version: 0
- major version: 50
- //常量池
- Constant pool:
- const #1 = class #2;
- const #2 = Asciz Test;
- const #3 = class #4;
- const #4 = Asciz java/lang/Object;
- const #5 = Asciz <init>; //实例构造器
- const #6 = Asciz ()V; //void返回类型
- const #7 = Asciz Code; //属性表Code属性
- const #8 = Method #3.#9; //方法特征签名 java/lang/Object."<init>":()V
- const #9 = NameAndType #5:#6;// 方法名称和返回值"<init>":()V
- const #10 = Asciz LineNumberTable; //属性表源码行号和字节码指令对应表
- const #11 = Asciz LocalVariableTable; //属性表方法局部变量表
- const #12 = Asciz this; //Test类实例对象本身
- const #13 = Asciz LTest;; //对象类型,Test类
- const #14 = Asciz getNum; //方法名称
- const #15 = Asciz (I)I; //方法参数列表为一个int类型和返回值为int类型
- const #16 = Asciz i; //参数名称i
- const #17 = Asciz I; //参数类型int
- const #18 = Asciz SourceFile;
- const #19 = Asciz Test.java;
- //方法表
- {
- //构造函数(默认构造方法)
- public Test();
- Code: //属性表Code属性
- Stack=1, Locals=1, Args_size=1
- 0: aload_0
- 1: invokespecial #8; //Method java/lang/Object."<init>":()V
- 4: return
- LineNumberTable:
- line 2: 0
- LocalVariableTable: //属性表方法局部变量表
- Start Length Slot Name Signature
- 0 5 0 this LTest;
- //自定义方法
- public int getNum(int);
- Code:
- Stack=2, Locals=2, Args_size=2
- 0: iload_1
- 1: iconst_1
- 2: iadd
- 3: ireturn
- LineNumberTable:
- line 4: 0
- LocalVariableTable:
- Start Length Slot Name Signature
- 0 4 0 this LTest;
- 0 4 1 i I
- }