c语言将.c文件编译生成与硬体相关的二进制机器指令,问题在于不同的系统中的机器指令不同,所以c语言所编写的程序可移植性差。而java号称一次编写,到处运行(Write Once,Run Anywhere),java语言将.java文件经过编译成固定格式的.class文件,并在JVM上执行来达到这种平台无关的效果。也就是说:JVM不与任何包括java在内的语言做绑定,JVM只与固定格式的二进制class文件关联。这也正是为什么Clojure、Groovy、JRuby、Scala这么多新性语言都是在JVM上运行的原因,因为他们只要最后编译生成class文件即可。那么.class文件具体结构是怎么样的呢?
数据类型
class是一个二进制文件,其中的数据都用无符号数表示,基本数据类型有4种:u1,u2,u3,u4。分别代表1、2、4、8四种字节的无符号数,这些无符号的数字用来表示各种字面量、符号引用、数值量或者utf-8构成的字符串值。除了上述四种基本数据类型还有一种复合数据类型:表。我们用_info结尾表示表结构,表是多个基本数据类型组合而成,用来表示多个层次上并列的关系的数据。实际上整个class文件也可以理解为一张表。
一个class文件的格式
类型 | 名称(CN) | 名称 | 数量 |
---|---|---|---|
u4 | 魔数 | magic number | 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文件是二进制文件,其中没有任何的多余修饰标记,所以任何一处错误都会使得整个文件发生错位,上述文件格式被严格的规定不允许任何的变动。
class文件使用魔数来标识它可以被java虚拟机识别。事实上魔数不是java发明的,很多格式的文件都会用魔数来标识文件类型,因为相比较容易被用户修改的用户名,魔数是编辑在二进制文件内部的难以修改。在class文件中采用的魔数是0xCAFEBABY。
在魔数后的四个字节是版本号,五六个字节是次版本号,七八个字节是主版本号。这四个字节合起来准确的表达了当前class所需的最低jdk版本。版本号和jdk的版本之间的关系如下(主要几个版本):
jdk版本 | class文件中版本号 |
---|---|
JDK 1.18 | 45.3 |
JDK 1.5.0_11 | 49.0 |
JDK 1.6.0_01 | 50.0 |
JDK 1.7.0 | 51.0 |
上面简单的列出了几个对应关系,并不全面但是我们需要知道的是,版本号主要大概在45.3-(50+)左右。
因为常量的数量是不固定的,所以在class文件中常量池首先由一个constant_pool_count来表示所有常量的数量。其后跟随一个共有constant_pool_count-1个数据信息的表。比如常量数量为8,则说明有7个常量(索引为1~7),之所以没有0索引是因为我们把0索引空出来表示“不引用任何常量池”的含义。这也反映出常量池计数constant_pool_count最小值为1,表示不引用任何常量池。
我们将字面量和符号引用两大类信息放入常量池中,作为常量信息存储起来。字面量表示文本字符串、声明为final类型的常量值。而符号引用则属于编译原理方面的概念,符号引用通常包括三大方面:
java在编译的过程中并没有c和c++的“连接”步骤,每个类之间并不能互通有无。java的处理方法是,在虚拟机加载class文件时将每个类有关类名、类方法名、字段名等信息的符号引用存放在方法区的常量池中,等到类被创建或者使用时解析将符号引用替换为直接引用。或许这么多的名词让你迷茫,总之简单的说,就是我们先用一个叫做符号引用的东西占位,当我们真正需要使用该类时在用正主把这个占位的替换掉,而这些所有的引用方式都存在于方法区的常量池中。
下面列举了14种常用的常量池的项目类型,包括字面量和引用两种:
常量池中14种常量项结构总表:
访问标志很简单,是用来表示此类的一些修饰符信息的。在常量池结束后,紧跟的两个字节信息就是访问标志,访问标志会唯一的标识出此类是否为public、是否定义为abstract、是否为final等信息。
类索引是一个u2类型数据,用来确定此类的全限定名。类索引通过指向一个类型为Constant_Class_Info来表述类的全限定名。
父类索引也是一个u2类型数据,用来确定此类的父类的全限定名。父类索引通过指向一个类型为Constant_Class_Info来表述此类的父类的全限定名,如果此类是Object类,则没有父类,该u2数据为0。
接口索引集合由一个接口索引数量interfaces_count来做计数。后面的interfaces_count个u2数据表示接口的索引全限定名。