java文件格式-类型是sol文件却不是flash sol格式文件
之前已经聊过了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,换算成十进制是52java文件格式,对应的JDK版本是JDK 8。
(3)常量池
版本号之后,紧接着就是常量池,常量池可以比作Class文件的资源仓库,是一个表类型数据项目,它和其他项目关联最多,通常也是占用空间最大的。
我们先从整体上了解下常量池的布局,如图所示:
常量池开始的两个字节,即class文件的第9、10个字节(目前还是固定位置,之后的都不固定了)代表常量池容量,容量字节之后依次存储了所有常量。
每个常量都是表结构,第一个字节都是tag,用来表示常量的类型,目前有17种类型(截至jdk13),随着jdk的升级常量类型也在不断增加。
参考上图,我们开放中常用到的类名,就是作为常量存储在class文件的常量池的,这个类名的常量的tag值为7,代表CONSTANT_Class_info类型。如图第三行可以看到java文件格式,它的第一个字节代表了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),方法表集合中就不会出现来自父类的方法信息。
编译器会自动添加方法,最常见的便是类构造器“()”方法和实例构造器“()”方法。
③重载
《Java语言规范》规定了Java方法的特性签名只包括方法名称、参数顺序及参数类型,因此Java的重载方法不考虑返回类型的因素。而字节码的特征签名还包括方法返回值以及受查异常表,也就是说,对于字节码而言,如果两个方法有相同的名称和特征签 名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。
(8)属性表
截至Java SE 12版本,预定义的属性有29项,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。
属性的定义方式各不相同,如需深入理解,可以查阅相关文章。以下列出部分属性定义。