JVM系列(十六):Class文件结构

本文详细介绍了Java字节码文件的跨平台特性,重点解析了Java虚拟机(JVM)如何使得多种语言能在其上运行。Java源码通过前端编译器如javac转换为符合JVM规范的字节码,字节码文件主要包括魔数、版本号、常量池、访问标志、类索引、字段表集合、方法表集合和属性表等。Class文件的结构严谨,数据项严格限定,无符号数和表是基本数据类型。此外,文章还讲解了如何使用javap指令解析Class文件,展示了Class文件内容的详细结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、概述

1.1、字节码文件跨平台性

1.1.1、Java语言:跨平台的语言(一次编译到处运行)

  • 无须多次编译
  • 由于Python、PHP等强大的解释器(天生就与平台无关),这个优势不再那么吸引人
  • 跨平台已经几乎是是语言的必备选项

1.1.2、Java虚拟机:跨语言的平台

  • Java虚拟机和Java语言没有绑定,仅和“Class”文件有关联
  • 任何语言,只要编译为正确的class文件,就可以在Java虚拟机上执行
  • 所有JVM都要遵守Java虚拟机规范

1.1.3、Java源码需要编译为符合JVM规范的字节码,才能正确运行在JVM中

  • 前端编译器就是将符合Java代码规范的Java代码转化为符合JVM规范的字节码文件
  • Javac就是一个将Java源码编译为字节码的前端编译器
  • 编译需要四个步骤:词法解析、语法解析、语义解析、生成字节码

1.2、Java前端编译器

1.2.1、前端编译器VS后端编译器

  • 前端编译器:Java代码转为字节码,如javac、ECJ(内置在Eclipse中的编译器)、ajc等,不会涉及编译优化
  • 后端编译器:JIT(即时编译器)
  • 举例:

  • 结果:

  • 解释:成员变量(实例变量、静态变量)赋值过程
  1. 默认初始化;
  2. 显式初始化;
  3. 构造器初始化;
  4. 有了对象之后,可以“对象.属性”或“对象.方法”对属性来赋值
  • 属性不具备多态特征

2、虚拟机的基石-Class文件

  • 字节码文件中存什么?字节码文件是一个二进制文件,其内容是JVM指令,但是不是像C/C++那样编译后产生的机器码
  • 字节码指令 = 操作码+操作数,也可以没有操作数
  • 查看Class文件方式:
  1. 一个一个二进制看,可以使用Notepad++,安装HEX-Editor插件
  2. 使用javap指令:javap –v 文件名
  3. 使用IDEA插件:jclasslib、jclasslib bytecode viewer客户端工具(效果好)

3、Class文件结构

  • Class类本质:任何class文件都对应着唯一的类或接口信息,class文件并不一定以磁盘文件形式存在,class文件是以8位字节为基础的二进制流
  • Class文件格式:不像XML文件,Class文件没有任何分隔符。所以数据项中,无论是顺序还是数量,都要严格限定,哪个字节代表什么含义、长度多少、先后顺序都不允许改变。Class文件采用类似C语言结构体的方式存储数据,结构中仅有两种数据类型:无符号数
  1. 无符号数:以u1、u2、u4、u8代表1个字节、2个字节、4个字节、8个字节的无符号数,无符号数可以描述数字、索引引用、数量值、按照UTF-8编码的字符串值
  2. 表:由多个无符号数或者表作为数据项组成的符合数据类型,习惯以“_info”结尾。用于描述有层次关系的复合结构数据,整个class文件本质就是一张表。由于表没有固定长度,所以在前面要加个数说明。
  • Class文件结构:魔数、class文件版本、常量池、访问标志、类索引-父类索引-接口索引集合、字段表集合(指的是类属性)、方法表集合、属性表(实际指的不是类属性)集合

3.1、魔数

  • 每个class文件开头前4个字节的无符号证书称为魔数
  • 唯一作用是确定该文件是否为合法的class文件,即魔数是class文件的标识符
  • 魔数值固定为0xCAFEBABE(cafebabe)
  • 如果class文件不以魔数开头,则会报错:

  • 使用魔数而不是拓展名来识别class文件,是为了安全,因为拓展名可以随意更改

3.2、Class文件版本号

  • 魔数之后的2个字节为Class文件版本号
  • 高版本虚拟机兼容低版本编译器,低版本虚拟机不兼容高版本编译器
  • 实际应用中注意开发编译JDK与生产环境的JDK是否一致

3.3、​​​​​​​常量池

  • 常量池是Class文件中内容最丰富的区域之一,常量池对于class文件中的字段和方法解析有重要作用
  • 常量池不断发展,内容越来越丰富,是整个Class文件的基石
  • 位于版本号之后,共2+n个字节,分别代表常量池计数器,以及常量池表(n个常量)

  • 常量池中常量的数量是不固定的,所以在常量池入口放一个u2类型无符号数,代表常量池容量计数值(constant_pool_count)。与java不同计数器从1开始,而不是0
  • 常量池表中,存放编译时生成的各种字面量符号引用,这些内容在运行时进入方法区中的运行时常量池。

3.3.1、常量池计数器

  • 常量池数量不固定,需要两个字节带表示常量池容量计数值
  • 计数器从1开始,而不是0,比如constant_pool_count=1表示0个常量项

  • 0x16代表十进制的22,所以有21个常量,索引范围是1-21
  • 常量池从1开始,因为要将第0项空出来,为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况用索引0表示

3.3.2、​​​​​​​常量池表

  • 主要存放字面量和符号引用
  1. 字面量:文本字符串、声明为final的常量值
  2. 符号引用:类和接口的全限定名、字段的名称和描述符(类型)、方法的名称和描述符
  • 全限定名:将类全名中的“. ”换成“/”,并在后面加上“;”表示全限定名结束(com.qdc.demo.Testàcom/qdc/demo/Test;)
  • 简单名称:指没有类型和参数修饰的方法或者名称字段,比如单纯的add()方法和num字段,简单名称分别为add和num
  • 描述符:用来描述字段的数据类型、方法的参数列表(数量、类型、顺序)、返回值

  • 虚拟机在加载Class文件的时候才会进行动态链接,所以在class文件中不保存方法和字段在内存中的布局信息,这些字段和方法在Class文件中是符号引用。当虚拟机运行时,要将从常量池中获得的符号引用在“类加载过程-链接-解析”阶段转为直接引用,并翻译为具体的内存地址
  1. 符号引用:用一组符号来描述所引用的目标,可以使任何形式的字面量,与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中
  2. 直接引用:可以是直接指向目标的指针、相对偏移量或者能够间接定位到目标的句柄。直接引用与虚拟机内存布局相关,有了直接引用,那么目标就已经在内存中了。
  • 常量类型和结构

3.4、​​​​​​​访问标识(access_flag

  • 位于常量池后,使用两个字节标识
  • 用于识别类或接口层次的访问信息,包括:
  1. Class是类还是接口
  2. 是否定义为public
  3. 是否定义为abstract
  4. 如果是类,是否为final类 …等等

3.5、​​​​​​​类索引、父类索引、接口索引集合

  • 位于访问标记后,用于指定该类的类别、父类类别、实现的接口(第三个是接口数量),格式如下:

  • 类索引可以用来确认这个类的全限定名
  • 父类索引用于确定父类的全限定名(隐含继承Object,Object类没有父类)
  • 接口索引描述实现了哪些接口,按implements语句后的顺序排列。使用数组保存多个接口的索引,表示接口的每个索引也是一个指向常量池的CONSTANT_Class

3.5.1、interfaces_count

  • interfaces_count的值代表当前类或接口的直接接口数量

3.5.2、interfaces

  • interfaces[interfaces_count]存储接口索引,

  • interfaces[0]代表implements参数后第一个接口(最左边接口)

3.6、​​​​​​​字段表集合

  • Fields
  1. 用于描述接口或者类中声明的变量,包含类变量(静态)、实例变量,不包含方法中的局部变量和代码块中的变量
  2. 字段的名字、类型都是无法固定的,只能引用常量池中的常量来描述
  3. 指向常量池索引集合,描述每个字段的完成信息,如:字段标识符、访问修饰符、static、final等
  • 注意事项
  1. 字段表集合中不会列出父类或者实现的接口中继承来的字段,但是可能会列出一些指向外部类实例的字段
  2. 字段无法重载(属性无多态),字段名称不能一样

3.6.1、fields_count

  • fields_count字段计数器,有多少个字段

3.6.2、fields[]:字段表

  • fields表中的每个成员都是一个fields_info结构的数据项,用于表示当前类或接口中某个字段的完成描述
  • 一个字段的信息如下:(boolean类型)作用域、static、final、volatile、transient(可否序列化)、字段数据类型、字段名称
  • 字段表结构(每个字段的结构):

  • 字段访问标志:
  1. 两个字节,16位
  2. (boolean类型)作用域、static、final、volatile、transient修饰符等

  • 字段名索引:
  1. 两个字节,16位
  2. 根据字段名索引中的值,去常量池中查询具体名称
  • 描述符索引:描述字段的数据类型、方法的参数列表(数量、类型、顺序)、返回值【方法??】。
  • 字段中属性集合(not important):一个字段可能有一些属性,用于存储更多额外信息。比如初始化值、一些注释信息等。属性个数存放在attribute_count(属性计数器)中,具体内容存放在attributes数组中

3.7、​​​​​​​方法表集合

  • 指向常量池索引的集合,完成描述每个方法的签名
  1. 字节码文件中,每一个method_info项都对应着一个类或接口的中的方法信息,比如修饰符(public、private等)、方法返回类型、参数信息等
  2. 方法是否是native、abstract,也会在字节码文件中体现出来
  3. Methods表只描述当前类或接口的方法,不包括父类或者实现的接口中的方法。Methods表可能会自动添加一些方法,比如编译器产生的clinit类/接口初始化方法、init实例初始化方法
  • 注意事项:
  1. Java中,重载的方法名可以一样,返回类型也可以一样,但是参数不能一样。其实可以理解为,重载要求新方法拥有一个与原方法不同的特征签名,特征签名是一个方法中各个参数在常量池中的字段符号的引用集合,所以返回值不会包含在特征签名中,因此Java语言无法依靠返回值来进行重载。
  2. 在Class文件中,特征签名的范围更大,只要描述符不一样就可以共存,比如即使两个方法名称和特征签名一样,但是返回值不一样,那么这两个方法也可以共存,因为返回值描述符不一样
  3. 所以Java语法中不允许在一个类中声明多个签名相同的方法,但是字节码中允许存在多个签名相同的方法,只要它们的返回值不一样

3.7.1、方法计数器

  • 记录有多少个方法

3.7.2、方法表

  • 结构如下:

3.8​​​​​​​、属性表集合

  • 位于方法表之后,指的是class文件所携带的辅助信息,比如Class文件的源文件名称。这个不需要深入了解
  • 方法表和字段表都拥有自己的属性表
  • 属性表集合的限制没有那么严格,不要求属性有严格的顺序,只要属性名不重复即可。任何人实现的编译器都可以在属性表中写入自己定义的属性信息,Java虚拟机会自动忽略它不认识的属性

3.8.1、属性计数器

3.8.2、属性表

  • 一共有23个属
  • 属性通用格式:

  • Code属性


4、使用javap指令解析Class文件

  • javac xx.java编译不产生本地变量表
  • javac –g xx.java编译产生本地变量表
  • javap –help查看javap的options选项

  • -p -private 显示大于等于private的属性和方法

  • -s 输出描述符

  • -c仅输出反汇编信息,-v输出反汇编和其它消息

  • javap –v –p xx.class能得到最全的信息(没有-p的话私有的不会输出)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值