与 JVM 做朋友系列 (1) 你好,Class 字节码
前提概要
俗话说,交朋友贵乎知心,我们需要了解它的性格特点,爱好、长处短处等,接下来让我们好好的了解一下这位好朋友的特点有哪些。接下来我会用“这位朋友”或者“它”代表 JVM(Java 虚拟机)。
它的特点
它可以跨平台
在我们世界里面,有着不同的国家和不同的语言以及不同的文化,而计算机的世界里面,也拥有着各种不同我的操作系统以及系统结构,而我们这位“朋友”它了解各种操作系统底层差异特点,“它”非常聪明的自学建立一套属于“它”自己的语言,这种语言可以与任何操作系统进行交流,而这门语言就是各大系统平台的虚拟机与所有平台都统一标准的程序存储格式——字节码(ByteCode)。
字节码(ByteCode)是构成平台无关性的基石,也是语言无关性的基础。 “它”不和包括
Java 源码在内的任何语言绑定,在我们眼里字节码的表现形式就是“Class 文件”,“它”只与“Class 文件”这种特定的二进制文件格式所关联, Class 文件中包含了 Java 虚拟机指令集和符号表以及若干其他辅助信息。所以我们一定要好好了解一下 Class 文件,才能更好的了解我们这位“朋友”。
Class 文件是啥
学过 Java 的都知道,运行的程序主要就是两大类:接口和类。而任何一个 Class 文件都表示着唯一一个类或接口的定义信息,但是 Class 文件实际上它并不一定以存储在磁盘文件的形式存在。它本质就是一组以 8 位字节为基本单位的二进制流。(如同人本质是由原子构成的一样)。
Class 文件格式
它的内部存储了各部分数据信息,严格按照顺序紧凑地排列在 Class 文件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数据,几乎没有空隙。(多么节省空间,勤俭持家)。
Class 文件格式采用一种类似于 C 语言结构体的结构模型来存储数据,这种伪结构中只有两
种数据类型:无符号数和表。
无符号数:属于基本的数据类型,以 u1、 u2、 u4、 u8 来分别代表 1 个字节、 2 个字节、 4
个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照
UTF-8 编码构成字符串值。
表:可以理解为数据的集合,由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一
张表。
Class 格式详解
Class 文件的结构不像 XML 等描述语言,由于它没有任何分隔符号,所以在其中的数据项,无
论是顺序还是数量,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序
如何,都不允许改变。
魔数头(第一部分)
魔术(Magic Number)[4 个字节]
Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。使用魔数而不是扩展名来进行识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。如同我们的用户名,如果不被接受,就无法被执行。CAFEBABE
文件格式制定者可以自由地选择魔数值,只要魔数值还没有被广泛采用过同时又不会引起混淆即可。
Class 版本(第二部分)
Class 文件(次版本、主版本)[2*2 个字节]
紧接着魔数的 4 个字节存储的是 Class 文件的版本号:第 5 和第 6 个字节是次版本号
(MinorVersion),第 7 和第 8 个字节是主版本号(Major Version)。 两个版本基本都属于 16 进制进行存储展示。所以每个属性都是两个字节。
Java 的版本号是从 45(十进制)开始的,JDK 1.1 之后的每个 JDK 大版本发布主版本号向上加 1 高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,“我们的朋友”也不会认可并且必须拒绝执行超过其版本号的 Class 文件。
版本的范围(十进制):jdk1 -> jdk8 = [45,52]
关系图如下:
常量池(第三部分)
常量池计数器[2 个字节]+常量池表(数据集合)
常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值(constant pool count)。与 Java 中语言习惯不一样的是,这个容量计数是从 1 而不是 0 开始的。此时我们可以存放 2^16 次方个常量数据。
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。
符号引用则属于编译原理方面的概念,包括了下面三类常量:类和接口的全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符
访问标志(第四部分)
访问标志(access flag)[2 个字节]+类索引[2 个字节]+父类索引[2 个字节]
所有的索引都是指向常量池里面的索引指针,而访问标志是一组数值列表,不同类型的数值。
用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等类索引、父类索引与接口索引集合
这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。
由于 Java 语言不允许多重继承,所以父类索引只有一个,除了 java.lang.Object 之外,所有的 Java 类都有父类,因此除了 java.lang.Object 外,所有 Java 类的父类索引都不为 0。
以下就是访问标志对应关系图:
接口信息(第五部分)
接口的数量[2 个字节]+接口索引表(主要属于指向常量池中元数据信息)
接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按 implements 语句(如果这个类本身是一个接口,则应当是 extends 语句)后的接口顺序从左到右排列在接口索引集合。
字段表集合(第六部分)
字段计数器[2 个字节]+字段索引表(主要属于指向常量池中元数据信息)
描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量。
字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池中常量来描述。
字段表集合中不会列出从超类或者父接口中继承而来的字段,但有可能列出原本 Java 代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
方法表集合(第七部分)
方法计数器[2 个字节]+方法索引表(主要属于指向常量池中元数据信息)
描述了方法的定义,但是方法里的 Java 代码,经过编译器编译成字节码指令后,存放在属性表集合中的方法属性表集合中一个名为“Code”的属性里面。
与字段表集合相类似的,如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“<clinit>”方法和实例构造器“<init>”
属性表集合(第八部分)
属性计数器[2 个字节]+属性索引表(主要属于指向常量池中元数据信息)
存储 Class 文件、字段表、方法表都自己的属性表集合,以用于描述某些场景专有的信息。如方法的代码就存储在 Code 属性表中。
以下就是对 Class 字节码的逻辑结构:
以下就是对 Class 字节码的结构总览:
对其评价
至此我们已经基本了解了 Class 字节码的样子大概是什么了,其实虽然很抽象并且结构很难理解,但是凭借着我们不断努力去了解和挖掘,我们很容易知道,这样子的好处以及“它”的好哦,以后我们在继续慢慢了解更多的它,所谓一口吃不成个胖子,慢慢来,别着急哦,未完待续......
版权声明: 本文为 InfoQ 作者【李浩宇/Alex】的原创文章。
原文链接:【http://xie.infoq.cn/article/274ce981e1c2b4c95d01efe6d】。文章转载请联系作者。
评论