深入理解JVM:类文件结构

来自Wikioe
Eijux讨论 | 贡献2020年10月25日 (日) 01:42的版本 →‎访问标志
跳到导航 跳到搜索


无关性的基石

   代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。


  • 无关性:即与平台无关,“一次编写,到处运行(Write Once,Run Anywhere)”;
  • 无关性的基石:字节码(Byte Code):
    各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式;


Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。

Java虚拟机提供的语言无关性.jpg

Class类文件的结构

   任何一个Class文件都对应着唯一的一个类或接口的定义信息,但是反过来说,类或接口并不一定都得定义在文件里(譬如类或接口也可以动态生成,直接送入类加载器中)。
  • Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文件之中,中间没有添加任何分隔符

根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:

  1. “无符号数”:基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
  2. “表”:由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。
    表用于描述有层次关系的复合结构的数据。
  • 整个Class文件本质上也可以视作是一张表。


Class文件格式:

Class文件格式.jpg

如上图,Class的没有任何分隔符,其数据项的顺序、数量,甚至于数据存储的字节序(Byte Ordering,Class文件中字节序为Big-Endian)等,都是被严格限定的,哪个字节代表什么含义,长度是多少,先后顺序如何,全部都不允许改变。


魔数、Class文件的版本

魔数(Magic Number):每个Class文件的头4个字节被称为魔数(Magic Number),唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。

  • 很多文件格式标准中都有使用魔数来进行身份识别的习惯,因为相对于文件后缀更加安全;(譬如图片格式,如GIF或者JPEG等在文件头中都存有魔数)
  • Class文件的魔数为:“0xCAFEBABE(咖啡宝贝?)”;


Class文件的版本号:紧接着魔数的4个字节存储的是Class文件的版本号;

  1. 第5和第6个字节是次版本号(MinorVersion)
  2. 第7和第8个字节是主版本号(Major Version)
  • Java的版本号是从45开始;
  • 高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件;
    如:JDK 1.1能支持版本号为45.0~45.65535的Class文件,无法执行版本号为46.0以上的Class文件,而JDK 1.2则能支持45.0~46.65535的Class文件。


示例: Java Class文件的结构.jpg 使用“WinHex”打开Class文件如上图:

  1. 开头4个字节的十六进制表示是 0xCAFEBABE
  2. 代表次版本号的第5个和第6个字节值为 0x0000,而主版本号的值为 0x0032,也即是十进制的50,该版本号说明这个是可以被JDK 6或以上版本虚拟机执行的Class文件。


Class文件版本号:

Class文件版本号.jpg

常量池

紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。


  • 由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count,从1而不是0开始的)。


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

  1. 字面量(Literal):接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等
  2. 符号引用(Symbolic References):属于编译原理方面的概念,主要包括下面几类常量:
    1. 被模块导出或者开放的包(Package)
    2. 类和接口的全限定名(Fully Qualified Name)
    3. 字段的名称和描述符(Descriptor)
    4. 方法的名称和描述符
    5. 方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic)
    6. 动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant)


  • 符号引用(如同之前内存部分所说):
    1. class文件中,方法调用其他方法,需要将其他方法的“符号引用”转化为直接引用,此过程即为“动态链接”;
    2. Java虚拟机栈的每个栈帧都有一个“指向运行时常量池中,该栈所属方法的”符号引用;
  • 常量池中每一项常量都是一个表:
    1. 共有17类表;
    2. 这17种常量类型各自有着完全独立的数据结构;
    3. 表结构起始的第一位是个u1类型的标志位(tag),代表着当前常量属于哪种常量类型:
      常量池的项目类型1.jpg
      常量池的项目类型2.jpg


常量池中的17种数据类型的结构总表:

常量池中的17种数据类型的结构总表1.jpg
常量池中的17种数据类型的结构总表2.jpg
常量池中的17种数据类型的结构总表3.jpg

访问标志(access_flags)

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final;等等。


访问标志:

访问标志.jpg
  • access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一律为零。

类索引、父类索引与接口索引集合

字段表集合

方法表集合

属性表集合

字节码指令简介

6.4.1 字节码与数据类型 6.4.2 加载和存储指令 6.4.3 运算指令 6.4.4 类型转换指令 6.4.5 对象创建与访问指令 6.4.6 操作数栈管理指令 6.4.7 控制转移指令 6.4.8 方法调用和返回指令 6.4.9 异常处理指令 6.4.10 同步指令

公有设计,私有实现

Class文件结构的发展