JVM-09-类文件结构

JVM-09-类文件结构

前序

计算机是由晶体管、电路板等组装而成的电子设备,而这些电子设备其实只能识别0与1的信号。

那么问题来了,在操作系统上编写的Java代码(由字母,数字等各种符号组成),打包后部署到服务器上,是如何被计算机所识别并运行的呢?

另外操作系统有很多种,包括Windows系统,Linux系统,Mac OS系统等,而我们同样的Java代码,却可以不做任何处理在不同系统上运行,这又是为什么呢?

带着这些疑问,你将在下面的内容中找到答案!!!

1. JVM的两个特性

1.1 语言无关

对于Java语言,我们通过编辑器编写的Java代码,后缀一般是.java。通过javac编译器编译后,会变成.class结尾的字节码文件,只有编译后的.class文件,才能在Java虚拟机上运行。(解压部署在服务器上的jar包,全是编译后的class文件)

再比如对于 JRuby 语言,通过编辑器编写的代码后缀是.rb。通过jrubyc 编译器编译后,也会变成 .class 结尾的字节码文件,然后也能在Java虚拟机上运行。

在比如已正式成为Android官方支持开发语言的Kotlin。也可以编译成.class字节码文件,然后在虚拟机上运行。

我们可以用下面这幅图来表示:

img

1.2 平台无关

Write once, run everywhere(一次编写,到处运行)

这是Java语言诞生之处就宣传的一个口号。Java语言之所以能够跨平台运行,其实就是因为Java虚拟机对各个平台的适配,在不同的系统下安装不同的Java虚拟机,我们程序当然能够在不同的系统上运行。

对于文章开头提出的问题,同样的程序能够在不同的系统上正常运行的原因,就是因为不同的操作系统上装了不同的JVM。

2. class 字节码文件介绍

搞清楚了Java代码的跨平台原理,我们接着来介绍为什么编写的Java代码能够被计算机所识别。

2.1 字节码文件

  • 这其实是上面所说的语言无关性这个特性重要文件——class字节码文件的功劳。
  • Java所有的指令大概有200个左右,一个字节(8位)可以存储256种不同的信息,我们将这样一个字节称为字节码(ByteCode)
  • 而class文件便是一组以8位字节为基础单位流的二进制流,各个数据项目严格按照顺序紧凑的排列在class文件之中,中间没有添加任何分隔符。
  • 所以整个class文件中存储的内容几乎都是程序运行时必要的数据,没有任何冗余。
  • 当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。

比如,对于如下这段代码:

1
2
3
4
5
6
7
public class ClassTest {
private static int i = 0;

public static void main(String[] args) {
System.out.println(i);
}
}

我们将生成的class 文件,通过十六进制编辑器打开(在IDEA中,可以下载HexView插件,安装完成后,选择这个class文件,右键 HexView)

img

打开后的文件如下:(下面的介绍也都是以这张图为例)

mark

下面我们会介绍这些十六进制分别代表什么意思。

2.2 javap命令

  • 另外,为了更好的查看 Class 文件字节码结构,JDK 还为我们提供了一个命令行工具 javap。使用语法如下:
1
javap <options> <classes>
  • 通过 javap -help 命令,可以查看相关参数作用:

mark

  • 我们将 ClassTest.class 文件,通过 javap -v ClassTest.class 命令,执行后如下:

mark

这些内容下面也会详细介绍。

3. 无符号数和表

在介绍这些十六进制之前,我们先介绍class文件的数据类型。

  • Class文件采用一种类似于C语言结构体的伪结构来存储,这种伪结构只有两种数据类型
    • 无符号数

3.1 无符号数

  • 这是一种基本数据类型,以u1,u2,u4,u8 来代表1个字节、2个字节、4个字节、8个字节的无符号数。

  • 无符号数可以用来描述数字,索引引用,数值量或按照UTF-8编码构成的字符串。

mark

3.2 表

  • 表是由多个无符号数或者其他表作为数据项所构成的符合数据类型,所有表都习惯行以”_info”为结尾。表用于描述有层次关系的复合结构数据。

整个class文件本质上就是一张表,结构如下:

mark

PS:

由于Class文件结构没有任何的分隔符,所以无论是每个数据项的顺序还是数量,都是严格限定的,哪个字节代码什么含义长度是多少先后顺序如何都是不允许改变的。

下面,我们就来分别介绍这些数据项代表什么含义。 

4. 魔数

  • 每个Class文件的头4个字节称为魔术(Magic Number)
  • 它的唯一作用是:标识该文件是一个Java类文件。如果没有识别到该标志,则说明该文件不是Java类文件或者文件已受损。

由上内存图可以看到的是,前四个字节是cafe babe。这是Gosling定义的一个魔法数,意思是Coffee Baby。

PS:

其实很多文件存储标准中都是以魔数进行身份识别的,比如gif和jpeg,使用魔数而不是使用扩展名来进行识别的主要基于安全考虑,因为文件扩展名可以随意改动。

5. Class 文件的版本号

紧随魔数的4个字节存储的是class文件的版本号

  • 第五和第六个字节是次版本号(Minor Version)
  • 第七和第八个字节是主版本号(Major Version)
  • Java的版本号是从 45 开始的,JDK1.1 之后的每个 JDK 大版本发布主版本号向上加1(JDK1.0JDK1.1使用了45.045.3的版本号),高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式未发生变化。
  • 上图中第5、6、7、8个字节为00 00 00 34 。换算成十进制是52,代表JDK8的内部版本号。

6. 常量池

紧随主版本号后的是常量池的入口,是class文件中第一个出现的表类型数据项目,也是占用class文件空间最大的项目之一,更是class文件结构中与其他项目关联最多的数据结构。

6.1 常量池容量计数值

  • 因为常量池中的常量数目是不固定的,所以在常量池的入口要放置意向 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。

PS:

注意常量池容量计数值从1开始的,而不是从0开始的。

将0空出来,是为了满足后面某些只想常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的意思。

  • Class文件中,只有常量池的容量是从1开始的,其他的集合类型都是从0开始的。

看上图的十六进制文件,常量池的容量计数为:0x0025,即十进制的37。这就表示常量池中有36项常量,索引值分别为1~36(通过上面的javap命令生成字节码文件可以很明显的看出来有36个)

mark

6.2 常量池的内容

常量池主要存放两大类常量

  • 字面量(Literal):字面量比较接近于Java语言层面中常量的概念,比如:文本字符串,被声明为final 的常量值等。
  • 符号引用(Symbolic References) : 符号引用属于编译原理方面的概念,包括下面三类常量
    • 类和接口的权限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符

需要说明的是:Java代码在进行javac 编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件的时候进行动态连接。

也就是说,在Class文件中不会保存各个方法和字段的最终内存布局信息,因此这些字段和方法的符号引用不经过转换的话是无法被虚拟机使用的。

当虚拟机运行的时候,需要从常量池中获得对应的符号引用,再在类创建时候解析并翻译到具体的内存地址中。关于类创建和动态连接的过程,将在下一篇中详细介绍。

常量池中的每一项都是一个表,在JDK1.8中共有14种结构各不相同的表结构数据,每个表结构的第一位是一个 u1 类型的标志位(tag,取值是1~18,缺少的标识位是2,13,14,17的数据类型)。代表当前这个常量属于哪种常量类型。

14 种常量类型所代表的具体含义如下:

mark

接着我们来看十六进制文件,紧跟常量池的数量是0x0a,这是一个标志位,0x0a的十进制数是10,查看常量池项目表接口可以得到,表示的类型是 CONSTANT_Methodref_info。

mark

也就是说,接下来的u2类型0x0006,其十进制值为6,紧跟后面的u2类型十六进制为0x0017,其十进制值为23,这两个都是索引值,分别指向第索引值是6的常量和索引值是23的常量。

整个十六进制字节码就不一一进行推导了,下面是各个数据类型的结构:

mark

mark

7. 访问标志

常量池结束之后用的两个字节表示访问标志(access_flags),这个表示用于识别一些类或者接口层次的访问信息。

包括:这个Class是类还是接口,是否定义为public类型,是否定义为abstract类型,如果是类的话,是否被声明是final等。

具体的标志位和标志含义如下:

mark

上面定义了8个标志位,但是我们说访问标志是一个 u2 类型,一共有32个标志位可以使用,如果出现了上图没有定义的标志位一律是0。

8. 类索引,父类索引和接口索引集合

类索引,父类索引和接口索引按顺序排列在访问标志位之后。

  • 类索引:用于确定这个类的全限定名,是一个 u2 类型的数据
  • 父类索引:用于确定这个类的父类全限类名,也是一个 u2 类型的数据。因为Java是单继承的,除了 java.lang.Object 类以外,所有的类都有父类。所以除了Object类之外,所有的Java类的父类索引都不为0。
  • 接口索引:用于描述这个类实现了哪些接口,是一个u2类型的数据集合,第一项是u2类型的接口计数器,表示实现接口的个数。如果没有实现任何接口,则为0。

9. 字段表集合

字段表(field_info):描述接口或类中声明的变量。(不包括方法内部声明的变量)

描述的信息包括:

  • 字段的作用域(public,protected,private修饰)
  • 是类级变量还是实例级变量(static修饰)
  • 是否可变(final 修饰)
  • 并分可见性(volatile修饰,是否强制主从读写)
  • 是否可序列化(transien修饰)
  • 字段数据类型(8种基本数据类型,对象,数组等引用类型)
  • 字段名称

前面五个修饰符,都是布尔值,用标志位来表示;后面两个字段名称和类型,都是无法固定的,只能引用常量池的常量来表示。

mark

access_flags 是一个 u2 类型,表示各种修饰符。

mark

10. 方法表集合

Class 文件存储格式中对方法的描述和字段的描述基本上是一致的。也是依次包括

  • 访问标志(access_flags)
  • 名称索引(name_index)
  • 描述索引符(descriptor_index)
  • 属性表集合数量(attributes_count)
  • 属性表集合(attributes)

mark

方法访问标志如下(access_flags):

mark

11. 属性表集合

在前面介绍了字段表集合,方法表集合中都包括了属性表集合(attributes),其实就是引用的这里。

根据《Java虚拟机规范第二版》中,预定义了 9 项虚拟机实现应当能够识别的属性。

mark

对于每一项属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info 来表示,其属性值的结构则是完全自定义的,只需要说明属性值所占用的位数长度即可。

参考文档:https://docs.oracle.com/javase/specs/jvms/se8/html/

打赏
  • 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!
  • © 2019-2022 Zhuuu
  • PV: UV:

请我喝杯咖啡吧~

支付宝
微信