[Class文件结构]1——概述
前言
众所周知,计算机只识别0和1,程序员写的c或c++程序最终都要经过编译、链接等步骤,将代码转换成0或1的二进制格式才能被计算机执行。由于依赖平台,导致c语音中基础数据类型在各平台上所占的字节数都不相同,其它细节也有不同。
Java不一样,Java在诞生之初就宣称“write once,run anywhere”,它选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。
实现语言无关性的基础就是虚拟机和字节码存储格式,使用Java编译器可以把Java代码编译为存储字节码的Class文件,使用JRuby等的编译器一样可以把代码文件编译成Class文件,虚拟机并不关心Class文件的来源,只要它符合Class文件就有的结构就可以在Java虚拟机中运行。
Class类文件的结构
解析学习Class类文件的结构,是了解虚拟机的重要基础之一,有助于学习虚拟机如何加载并执行class文件。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符。
Class文件采用一种类似C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。
无符号数基于基本的数据类型,以u1,u2,u4,u8来分别代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或按照UTF-8编码构成的字符串值。
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时候称这一系列连续的某一类型的数据为某一类型的集合。
注意:Class文件不像xml,它没有分隔符,而且它的顺序都是严格定义的。
Java代码中包含成员变量,方法,变量还定义了访问权限,方法还有参数、返回值,方法中的逻辑(比如说int型数据相加)是否会被存储成一个个指令,Class文件会如何描述这些东西呢?
示例代码
以如下代码为示例,查看编译后的Class文件。
package org.fenixsoft.clazz;
public class TestClass {
private int m;
public int inc(){
return m + 1;
}
}
Class文件是字节码文件,需要使用16进制编辑器查看,本文中选用 WinHex,打开Class文件后整体如下:
另外Class文件可以使用javap工具查看,指令如下:javap -verbose class文件路径
魔数与Class文件版本
每个Class文件的头4个字节称为魔数,它的唯一作用是用于确定这个文件是否为Class文件。查看上单节的Class文件格式图,魔数正好也是由一个u4类型表示。
Class文件的魔数为 cafebabe(咖啡宝贝?)。魔数之后即是次版本号和主版本号,二者均由一个u2表示。
本例中次版本号为0,主版本号为51(33是16进制数,换成10进制就是51),即为Java 1.7。注意高版本的JDK能向下兼容,但无法运行以后版本的Class文件,那么本例中的Class文件只能被JDK 1.7及以上运行。
常量池
主版本号后边就是常量池了,常量池中的所有项目类型为:
使用javap -verbose 查看Class文件,查看常量池部分
常量池中存放两大类常量:字面量和符号引用。字面量,如字符串、被声明为final的常量值等。而符号引用(符号引用属于编译原理方面的概念)包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
和Java代码中的常量不一致,常量池中并不是只会存储final类型的数据,不是常量也会被存储,比如示例代码中的m,另外符号引用也并不是代码中所说的引用类型,除了类和接口,它还包含字段和方法。
虚拟机在加载Class文件时进行动态连接,也就是说Class文件并不会保存各个方法和字段的最终内存布局信息,当虚拟机运行时,需要从常量池中获取对应的符号引用,在运行时解析并翻译到具体的内存地址当中。
常量池是最繁琐的数据了,因为常量池中所包含的11种类型都有自己的结构。一般是由一位u1指定类型,再加上数据长度(一般是u2),再加上数据具体值。