Java编译之Class文件结构

之前已经聊过了Java的运行体系,这期主要了解下编译产生的Class文件。

1.平台无关性

Java诞生时提出一个著名的口号“一次编写,到处运行(Write Once, Run Anywhere)”,而这一特性的实现基础就是字节码(Byte Code)。各种不同平台的Java虚拟机和统一的程序存储格式字节码,是Java平台无关性的基石。

Java中的各种语法、关键词、变量和运算符号等,最终都会被编译成多条字节码指令组合,因此字节码所能提供的能力要比Java强大很多。字节码一般存储在Class文件中,但并非一定,也可以通过其方式存储和获取。

Java虚拟机其实并不和Java程序语言绑定,而是和字节码文件(一般指Class文件)所关联。现在可以运行在Java虚拟机之上的除Java外的语言有很多,包括Kotlin、JRuby、Groovy、JPython、scala等。

2.Class文件结构

(1)概述

了解Class文件结构可能会比较枯燥,但这是虚拟机的重要基础之一,是深入了解虚拟机的必经之路。

一个Class文件对应一个类或者接口的定义,但类或者接口定义不一定必须定义在文件里(可网络传输或动态生成)。

Class文件以8个字节为基础单位的二进制流,各个数据项严格按照规定的长度和顺序紧凑地排列在文件中,中间没有任何分隔符。

Class文件中只有两种数据类型:“无符号数”和“表”。

无符号数:基本数据类型,以u1、u2、u4、u8代表1个字节、2个字节、4个字节、8个字节的无符号数,可用来描述数字、索引引用、数量值或者UTF-8编码的字符串。

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

Class文件也可以看作是一张表,它的数据项严格按照以下顺序构成。

 

(2)魔数和版本

使用WinHex打开一个class文件,再看下文件的内容吧。

 

前四个字节0xCAFEBABE(咖啡宝贝,这个标识颇有浪漫气息)就是class文件的魔数,唯一作用是识别文件是否可以被虚拟机接收。之所以没有通过扩展名标识,是出于安全考虑,因为扩展名容易被修改。

紧接着的5、6字节是次版本号,7、8字节是主版本号。如图中的7、8字节0x0034,换算成十进制是52,对应的JDK版本是JDK 8。

(3)常量池

版本号之后,紧接着就是常量池,常量池可以比作Class文件的资源仓库,是一个表类型数据项目,它和其他项目关联最多,通常也是占用空间最大的。

我们先从整体上了解下常量池的布局,如图所示:

常量池开始的两个字节,即class文件的第9、10个字节(目前还是固定位置,之后的都不固定了)代表常量池容量,容量字节之后依次存储了所有常量。

每个常量都是表结构,第一个字节都是tag,用来表示常量的类型,目前有17种类型(截至jdk13),随着jdk的升级常量类型也在不断增加。

参考上图,我们开放中常用到的类名,就是作为常量存储在class文件的常量池的,这个类名的常量的tag值为7,代表CONSTANT_Class_info类型。如图第三行可以看到,它的第一个字节代表了tag标识了类型,而之后并没有直接存储类的名称字符串,而是用两个字节存储了一个常量池的索引值。

这个索引值会指向一个CONSTANT_Utf8_info类型的字符串常量(如第二行所示),字符串常量存储了这个类的名称。Class文件正是通过这种互相引用的间接方式来存储常量的。

整体的了解常量池之后还需要掌握以下几个点:

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

字面量,接近Java语言常量的概念,比如文本字符串、被声明为final的常量值等。

符号引用,属于编译原理方面的概念,包括:包、类和接口的全限定名、字段的名称和描述符、方法的名称和描述符、方法句柄和方法类型、动态调用点和动态常量等。

② 常量池的项目类型

每种常量类型的结构定义是不同的,需要深入了解的,可以查阅相关文档。

③ 常量池的索引从1开始

常量池的索引是从1开始的,而不是我们习惯的0开始。这样做的目的在于,如果后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,可以把索引值设置为0来表示。

④ 方法名和字段名的最大长度

由于Class文件中方法、字段等都需要引用CONSTANT_Utf8_info型常量来描述名称,而CONSTANT_Utf8_info型常量的最大长度是u2类型的最大值65535,所以Java程序中如果定义了超过64KB 英文字符的变量或方法名,即使规则和全部字符都是合法的,也会无法编译。

(4)访问标志

常量池之后用u2存储了访问标志(access_flags),用来表示这个Class是类还是接口,是否为public,是否为abstract等。

access_flags有两个字节16位,它是按照每bit位是1还是0来标志是否是某项类型的,然后通过各位或的方式进行组合标志。如某类的ACC_PUBLIC位和ACC_SUPER位为真,其它为假,那他的access_flags值为:0x0001|0x0020=0x0021。

(5)类索引、父类索引、接口索引

在访问标志之后,依次放置了类索引(u2)、父类索引(u2)和接口索引。

对于类索引和父类索引,它们均指向类型为CONSTANT_Class_info的类描述符常量,然后再间接找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。

对于接口索引,入口有一个u2类型的“接口计数器(interfaces_count)”,如果类没有任何接口,则此值为0,后面无任何字节占用,否则占用interfaces_count个u2来存储接口索引。

(6)字段表集合

类似的,紧随其后的是一个u2存储的字段长度fields_count,随后存储fields_count个字段表。

字段表的结构如下图所示,依次存储访问标志、字段名称索引、描述索引、属性表集合。

① 访问标志(access_flags

和之前介绍的类的access_flags类似,用每位标志字段的不同类型,包括是否public、以及是否private、protected、static、final、volatile、transient、enum。

② 简单名称(name_index)

如字段

private int age;

它的简单名称就是指age。

③ 描述符(desrptor_index)

对于类而言,类的简单名称就是类名如TestClass,而它的全限定名为“org/fenixsoft/clazz/TestClass”,把“.”改成“/”即可。

对于字段和方法来说,它们是通过描述符来表示的,而描述符的规则要相对复杂些。

首先描述符对基本数据类型进行了描述定义:

l 对于数组类型,每一维度将使用一个前置的“[”字符来描述;

l 用描述符来描述方法时,按照先参数列表、后返回值的顺序描述。

我们可以通过几个例子来理解描述符的规则:

void inc()的描述符:()V

int indexOf(char[]source,int sourceOffset,int sourceCount,char[]target,int targetOffset,int targetCount,int fromIndex)的描述符:“([CII[CIII)I”。

④ 属性表集合(attribute_info)

字段表所包含的固定数据项目到descriptor_index为止就全部结束了,不过在descriptor_index之后跟随着一个属性表集合,用于存储一些额外的信息,字段表可以在属性表中附加描述零至多项的额外信息。如final类型的字段,会存储一个ConstantValue的属性,通过常量索引指向一个常量。

⑤ 字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段。

(7)方法表

方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)。

① Java方法里的代码

Java的定义有访问标志、名称给索引、描述符索引表示,方法的内容存储在一个名字叫“Code”的属性里面。

② 方法重写

如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现来自父类的方法信息。

编译器会自动添加方法,最常见的便是类构造器“<clinit>()”方法和实例构造器“<init>()”方法。

③ 重载

《Java语言规范》规定了Java方法的特性签名只包括方法名称、参数顺序及参数类型,因此Java的重载方法不考虑返回类型的因素。而字节码的特征签名还包括方法返回值以及受查异常表,也就是说,对于字节码而言,如果两个方法有相同的名称和特征签 名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

(8)属性表

截至Java SE 12版本,预定义的属性有29项,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

属性的定义方式各不相同,如需深入理解,可以查阅相关文章。以下列出部分属性定义。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

目标Web3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值