本文是《深入理解Java虚拟机》第二版 中类文件结构一章的读书笔记。
6.1概述
原来由于计算机只认识0和1,所以我们写的程序需要经过编译翻译成0和1构成的二进制格式才能由计算机执行.10多年时间过去了,计算机还是只认识0和1,但由于最近10年内虚拟机及大量建立在虚拟机之上的程序语言如雨后春笋般出现并蓬勃发展,将我们编写的程序编译成二级制本地机器码已不再是唯一的选择,越来越多的程序语言选择了与操作系统和机器指令集无关的,平台中立的格式作为程序编译后的存储格式
6.2 无关的基石
在 Java 发展之初,设计者就曾经考虑过并实现了让其他语言运行在 Java 虚拟机之上的可能性,他们在发布规范文档的时候,也刻意把 Java 的规范拆分成了 Java 语言规范及 Java 虚拟机规范。
时至今日,商业机构和开源机构已经在java语言之外发展处一大批在java虚拟机之上的允许的语言,如Clojure,Groovy,Jruby,Jython,Scala等,java虚拟机不和包括java在内的任何语言绑定,它只与"class文件"这种特定的二进制文件格式所关联,例如,使用java编译器可以把java代码编译为存储字节码的class文件,使用JRuby等其他语言的编译器一样可以把程序代码编译成class文件,虚拟机不关心class的来源是何种语言,如图1-1所示
图1-1
6.3 Class文件数据结构
Class文件是一组以8bit为基础单位的二进制流,各个数据项目严格按顺序紧凑地排列在class文件中,中间无任何添割符。Class文件格式采用一种类似于C语言结构体的伪结构来储存数据,这种伪结构只有两种数据:无符号数和表。
无符号数属于基本的数据类型,以u1,u2,u3,u4,u8表示1个字节,2个字节,3个字节,4个字节,8个字节,无符号数可以用来描述数字,索引引用,数量值或者按照UTF-8编码构成字符串。
表是由多个无符号数或者其它表作为数据项构成的复合数据类型,所有表都习惯地以"_info"结尾。表用于描述由层次关系的复合结构的数据,整个class文件本质上就是一张表。它由表1-2所示的数据项构成
class文件格式
图1-2
无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时称这一系列连续的某一类型的数据为某一类型的集合 .
接下来根据案列讲解一些概念:
新建一个简单的Hello.java类,代码如下
代码清单6-1:
public class Hello {
public static void main(String[] args) {
int a = 1;
int b = 2;
int c = a + b;
System.out.println(c);
}
}
利用javac命令编译Hello.java,生成Hello.class文件,再用 ClassViewer打开Hello.class文件,如图1-3所示
图1-3
其中两位16进制数表示一个字节
6.3.1 魔数
每个Class文件的头4个字节称为魔数,它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件储存标准中都使用魔数来进行身份识别,譬如图片格式,如 gif 或者 jpeg 等在文件头中都存有魔数。
使用魔数而不是扩展名来仅从识别主要是基于安全方面的考虑,因为文件扩展名可以随意地改动。文件格式的制定者额可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。
Class 文件的魔数的获得很有“浪漫气息”,值为:0xCAFEBABE(咖啡宝贝?),这个魔数值再 Java 还称做“Oak”语言的时候(大约是 1991 年前后)就已经确定下来了。
紧接着魔数的4个字节储存的是Class文件的版本号:第5和第6个字节是次版本号,第7和第8个字节是主版本号。Java版本号是从45开始的,JDK1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0 ~ 1.1 使用了 45.0 ~ 45.3 的版本号),
高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。
例如,JDK 1.1 能支持版本号为 45.0 ~ 45.65535 的 Class 文件,无法执行版本号为 46.0 以上的 Class 文件,而 JDK 1.2 则能支持 45.0 ~ 46.65535 的 Class 文件。 JDK 版本为 1.7,可生成的 Class 文件主版本号最大值为 51.0
就Hello.class文件而言,代表次版本号的第5个和第6个字节为0X0000,而主版本号的值为0X0034,也就是十进制的52,该版本号说明这个文件是可以被jdk1.8或者以上版本的虚拟机的class文件,图1-4列出了从jdk1.1到jdk1.8 class文件版本与jdk版本对应的关系
图1-4
6.3.2常量池
紧接着主次版本号之后的是常量池入口,常量池可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中的常量数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数器。与Java中语言习惯不一样的是,这个容量计数是从1而不是0开始的,常量池容量为十六进制数0x002b,即十进制的43,这就代表常量池中有42项常量,索引值范围为1~42.在Class文件格式规范制定之时,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况就可以吧索引值置为0来表示。Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都一般习惯相同,是从0开始的。
常量池中主要存放两大类常量:字面量和符号引用。字面量比较接近于Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括了下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
Java代码在进行Javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在虚拟机加载Class文件中不会保存各个方法、字段的最终内存布局信息,因此这些字段、方法的符号引用不经过运行转换的话无法得到真正的内存入口地址,也就无法直接被虚拟机使用。当虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
使用JavaP命令输出Hello.class常量表
代码清单6-2
Constant pool:
#1 = Methodref #11.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#22 // java/lang/System.out:Ljava/io/Print
Stream;
#3 = Class #23 // java/lang/StringBuilder
#4 = Methodref #3.#20 // java/lang/StringBuilder."<init>":()
V
#5 = String #24 // c=
#6 = Methodref #3.#25 // java/lang/StringBuilder.append:(Lja
va/lang/String;)Ljava/lang/StringBuilder;
#7 = Methodref #3.#26 // java/lang/StringBuilder.append:(I)L
java/lang/StringBuilder;
#8 = Methodref #3.#27 // java/lang/StringBuilder.toString:()
Ljava/lang/String;
#9 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/
lang/String;)V
#10 = Class #30 // Hello
#11 = Class #31 // java/lang/Object
#12 = Utf8 <init>
#13 = Utf8 ()V
#14 = Utf8 Code
#15 = Utf8 LineNumberTable
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 SourceFile
#19 = Utf8 Hello.java
#20 = NameAndType #12:#13 // "<init>":()V
#21 = Class #32 // java/lang/System
#22 = NameAndType #33:#34 // out:Ljava/io/PrintStream;
#23 = Utf8 java/lang/StringBuilder
#24 = Utf8 c=
#25 = NameAndType #35:#36 // append:(Ljava/lang/String;)Ljava/la
ng/StringBuilder;
#26 = NameAndType #35:#37 // append:(I)Ljava/lang/StringBuilder;
#27 = NameAndType #38:#39 // toString:()Ljava/lang/String;
#28 = Class #40 // java/io/PrintStream
#29 = NameAndType #41:#42 // println:(Ljava/lang/String;)V
#30 = Utf8 Hello
#31 = Utf8 java/lang/Object
#32 = Utf8 java/lang/System
#33 = Utf8 out
#34 = Utf8 Ljava/io/PrintStream;
#35 = Utf8 append
#36 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#37 = Utf8 (I)Ljava/lang/StringBuilder;
#38 = Utf8 toString
#39 = Utf8 ()Ljava/lang/String;
#40 = Utf8 java/io/PrintStream
#41 = Utf8 println
#42 = Utf8 (Ljava/lang/String;)V
6.3.3 访问标志
访问标志的位置:在常量池结束之后的两个字节(16位)表示访问标志access_flags。
访问标志的作用:用于标识类或者接口层次的访问信息;比如该Class是类还是接口,是否为public类型、是否为abstract类型、是否是final类型等等。
在常量池结束之后,紧接着的两个字节代表访问标志,这些标志用于识别一些类或者接口层次的访问信息,包括:这个class是类还是接口;是否定义为public类型;
是否定义为abstract类型;如果是类的话.是否被声明为final等,具体标志位和标志的含义见表1-5
图1-5
access_flag中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位要求一律为0,以代码清单6-1中的代码为例,Hello.class是一个普通的java类,不是接口,枚举或者注解,被public关键字修饰但没有被声明为final和abstract,并且它使用了jdk1.2后的编译器进行编译,因此它的acc_public,acc_super标志应当为真,而acc_final,acc_interface,acc_abstract,acc_synthetic,acc_annotation,acc_enum这6个标志应当为假,因此它的access_flags的值应为0X0001|0x0020=0x0021,使用classViewer,点击左侧的access_flags,右侧的2个字节00 21变成高亮,
图1-6
下面我们根据下图1-7和1-8来手动计算一下,acc_public对应十六进制0x0001 ,acc_super对应十六进制0x0020,两个相加,发现与上述字节码对应的值是一样的
图 1-7
图1-8
6.3.4类索引, 父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺序从左到右排列在接口索引集合中。
类索引、父类索引和接口索引集合都按顺序排列在访问标志之后,类索引和父类索引用两个u2类型的索引值表示,它们各自指向一个类型为CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info类型的常量中的索引值可以找到定义在CONSTANT_Utf8_info类型的常量中的全限定名字符串。图6-6演示了代码清单6-1的代码的类索引查找过程。对于接口索引集合,入口的第一项——u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0,后面接口的索引表不再占用任何字节。代码清单6-1中的代码的类索引、父类索引与接口表索引的内容如图6-7所示。
从偏移地址0x000000F1开始的3个u2类型的值分别为0x000A、0x000B、0x0000,也就是类索引为1,父类索引为3,接口索引集合大小为0,查询前面代码清单6-2中javap命令计算出来的常量池,找出对应的类和父类的常量,结果如代码清单6-3所示。
代码清单6-3 部分常量池内容
#10 = Class #30 // Hello
#11 = Class #31 // java/lang/Objec
在访问标志符之后的6个字节,分别表示类索引,父类索引,接口索引集合大小,0X000A表示类索引为10,0X000B表示父类索引为11, 接口索引集合数为0X0000
6.3.5 字段表集合
参考博客https://www.cnblogs.com/lrh-xl/p/5350612.html
6.3.6 方法表集合
参考 https://www.cnblogs.com/lrh-xl/p/5351182.html
参考博客:
https://blog.csdn.net/wobushixiaobailian/article/details/83117373
https://www.cnblogs.com/huangjuncong/p/8999989.html
https://www.cnblogs.com/flyingcr/p/10428299.html
https://www.jb51.net/article/116204.htm
本人刚开始写博客,如有问题,请联系我, 觉得本文不错,顺手点个赞哦,您的鼓励,是我继续分享知识的强大动力!