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
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 (Unsupported 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编译时如果没有给定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字段包括了类级变量(静态变量)和实例级变量(成员变量),但不包括方法内部的局部变量。
field_count字段数目表示Class文件中的类和实例变量总数,字段存放的信息包括:字段访问标志、是否静态、是否final、是否并发可见volatile、是否可序列化transient、数据类型、字段名称等等。
注意:字段表中不包含从父类或者接口中继承而来的字段,但是会添加原本代码中不存在的字段,例如this,以及内部类对外部类访问而自动添加的外部类实例字段等。
(7)method:
method_info方法表用于描述类或者接口中声明的方法,methods_count用于表示Class文件中方法总数,method方法存储了方法的访问标识、是否静态、是否final、是否同步synchronized、是否本地方法native、是否抽象方法abstract、方法返回值类型、方法名称、方法参数列表等信息。
方法的代码指令并没有直接存放在方法表中,而是存放着属性表中的方法表Code中。
注意:如果父类的方法在子类没有被重写,方法表中不会出现来自父类的方法信息,但是编译器会自动添加类构造器"<clint>"方法和实例构造器"<init>"方法。
java编译器的方法特征签名只包括:方法名称、参数顺序和参数类型,不包括方法返回值类型,因此java的方法重载不能通过方法的返回值来区别,但是在Class文件中,方法特征签名包括方法的返回值类型,因此Class文件中可以共存两个名称和参数完全相同而返回值类型不同的方法。
(8)attribute:
attribute_infor属性表是Class文件格式中最具扩展性的一种数据项目,用于存放field_info字段表、method_info方法表以及Class文件的专有信息,属性表不要求各个属性有严格顺序,只要求不与已有的属性名字重复即可,属性表中存放的常用信息如下:
属性名称 使用位置 含义
Code 方法表 java代码编译后的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Deprecated 类、方法表、字段表 被声明为Deprecated的字段或方法
Exception 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 java源码行号和字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 源文件名称
Synthetic 类、方法表、字段表 标识方法或字段为编译器自动生成
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