Java虚拟机(四)-Class文件结构
1.Java语言的平台无关性和JVM的语言无关性
Java语言的平台无关性
《深入理解Java虚拟机-第二版》中第6章开头就写到:
代码编译的结果从本地机器码转变为字节码,是存储格式发展的一小步,却是编程语言发展的一大步。
为什么这么说呢?作者下面也解释的很明白。因为在虚拟机出现之前,程序要想正确运行在计算机上,首先要将代码编译成二进制本地机器码,而这个过程是和电脑的操作系统OS、CPU指令集强相关的,所以可能代码只能在某种特定的平台下运行,而换一个平台或操作系统就无法正确运行了。随着虚拟机的出现,直接将程序编译成机器码,已经不再是唯一的选择了。越来越多的程序语言选择了与操作系统和机器指令集无关的、平台中立的格式作为程序编译后的存储格式。Java就是这样一种语言。“一次编写,到处运行”于是成立Java的宣传口号。
正是虚拟机和字节码(ByteCode)构成了平台无关性的基石,从而实现“一次编写,到处运行”
Java虚拟机将.java文件编译成字节码,而.class字节码文件经过JVM转化为当前平台下的机器码后再进行程序执行。这样,程序猿就无需重复编写代码来适应不同平台了,而是一套代码处处运行,至于字节码怎样转化成对应平台下的机器码,那就是Java虚拟机的事情了。
JVM的语言无关性
Java语言通过JVM虚拟机和字节码(ByteCode)实现了平台无关性,那么语言无关性又是什么意思?其实,在Java虚拟机设计之初,作者非常前瞻性的说过:
“In the future,we will consider bounded extensions to the Java virtual machine to provide better support for other languages” 在未来,我们会对java虚拟机进行适当的拓展,以便更好的支持其他语言运行于JVM之上。
时至今日,商业机构和开源机构以及在Java语言之外发展出一大批在Java虚拟机之上运行的语言,如Groovy,JRuby,Jython,Scala等等。这些语言通过各自的编译器编译成为.class文件,从而可以被JVM所执行。
所以,由于Java虚拟机设计之初的定位,以及字节码(ByteCode)的存在,使得JVM可以执行不同语言下的字节码.class文件,从而构成了语言无关性的基础。或许在未来,语言无关性的优势会赶超Java平台无关性的优势。
2.字节码.class文件的结构
知识准备
在详细分析class文件结构之前,我们需要了解一些基本概念:
- class文件以8字节为基本单位来进行存储,中间没有任何分隔符;
- 当数据项需要占用的空间大于8字节时,会按照高位在前的方式来进行分割;
- class文件只有两种数据类型:无符号数、表;
- 无符号数属于基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数;
- 表是由多个无符号数或者其它表作为数据项构成的符合数据类型,表名习惯性都以 _info 结尾。
因此本质上整个class文件就是一张表,它由以下数据项构成:
类型 | 名称 | 数量 | 描述 |
---|---|---|---|
u4 | magic | 1 | 魔数 |
u2 | minor_version | 1 | 次版本号 |
u2 | major_version | 1 | 主版本号 |
u2 | constant_pool_count | 1 | 常量个数 |
cp_info | constant_pool | constant_pool_count – 1 | 具体常量 |
u2 | access_flags | 1 | 访问标志 |
u2 | this_class | 1 | 类索引 |
u2 | super_class | 1 | 父类索引 |
u2 | interfaces_count | 1 | 接口索引 |
u2 | interfaces | interfaces_count | 具体接口 |
u2 | fields_count | 1 | 字段个数 |
field_info | fields | fields_count | 具体字段 |
u2 | methods_count | 1 | 方法个数 |
method_info | methods | methods_count | 具体方法 |
u2 | attributes_count | 1 | 属性个数 |
attribute_info | attributes | attributes_count | 具体属性 |
可以看到,这16种数据项大致可以分为3类:
- 3个描述文件属性的数据项:魔数和主次版本号
- 11个描述类属性的数据项:类、字段、方法等信息
- 2个描述代码属性的数据项
接下来我们就逐一来看看这些数据项的含义。整个分析过程我们将以下面这段代码对应的class文件为基础:
public class JavaTest { private static String name = "JVM"; public static void main(String[] args) { System.out.println("Hello " + name); } }
1.魔数(4个字节)
每个class文件的头4个字节称为魔数,用于确定这个文件是否能被虚拟机所接受。值为:0xCAFEBABE-(4个字节)
2.版本号(4个字节)
第5、6字节为次版本号,7、8字节为主版本号。Java的主版本号从45开始,JDK1.1之后每个大版本发布,主版本号加1。高版本的jdk能前向兼容之前版本的class文件,但不能运行以后版本的class文件。
- minor_version 次版本号 (2个字节)
- major_version 主版本号 (2个字节)
- JDK向下兼容,向上不兼容
3. 常量池
- constant_pool_count:常量池容量计数值(2个字节)
- 计数从1开始,第0项空出来
- 常量池计数器 = 常量池表中的成员数 + 1. 常量池的索引值index,只有在index > 0 && index <constant_pool_count时才会认为是有效的
- 常量池主要用来存放字面量(Literal)和符号引用(Symbolic Reference)
- 字面量如文本字符串,声明为final的常量
- 符号引用包括:
- 类和接口的全限定名(Full Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
- 常量池中每一个常量都是一个表
- 表结构各不相同
- 都有一个u1类型的标志位(tag),代表当前常量属于的数据类型
- 用
javap -verbose Class
可以输出常量表,例如
javap -v JavaTest.class
Classfile /Users/xxx/Downloads/JavaTest.class
Last modified 2018-3-17; size 842 bytes
MD5 checksum fbb2370c6b7413a0636806a0e492224a
Compiled from "JavaTest.java"
public class com.princeli.JavaTest
minor version: 0
major version: 49
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #12.#29 // java/lang/Object."<init>":()V
#2 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #32 // java/lang/StringBuilder
#4 = Methodref #3.#29 // java/lang/StringBuilder."<init>":()V
#5 = String #33 // Hello
#6 = Methodref #3.#34 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#7 = Fieldref #11.#35 // com/princeli/JavaTest.name:Ljava/lang/String;
#8 = Methodref #3.#36 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#9 = Methodref #37.#38 // java/io/PrintStream.println:(Ljava/lang/String;)V
#10 = String #39 // JVM
#11 = Class #40 // com/princeli/JavaTest
#12 = Class #41 // java/lang/Object
#13 = Utf8 name
#14 = Utf8 Ljava/lang/String;
#15 = Utf8 <init>
#16 = Utf8 ()V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 Lcom/princeli/JavaTest;
#22 = Utf8 main
#23 = Utf8 ([Ljava/lang/String;)V
#24 = Utf8 args
#25 = Utf8 [Ljava/lang/String;
#26 = Utf8 <clinit>
#27 = Utf8 SourceFile
#28 = Utf8 JavaTest.java
#29 = NameAndType #15:#16 // "<init>":()V
#30 = Class #42 // java/lang/System
#31 = NameAndType #43:#44 // out:Ljava/io/PrintStream;
#32 = Utf8 java/lang/StringBuilder
#33 = Utf8 Hello
#34 = NameAndType #45:#46 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#35 = NameAndType #13:#14 // name:Ljava/lang/String;
#36 = NameAndType #47:#48 // toString:()Ljava/lang/String;
#37 = Class #49 // java/io/PrintStream
#38 = NameAndType #50:#51 // println:(Ljava/lang/String;)V
#39 = Utf8 JVM
#40 = Utf8 com/princeli/JavaTest
#41 = Utf8 java/lang/Object
#42 = Utf8 java/lang/System
#43 = Utf8 out
#44 = Utf8 Ljava/io/PrintStream;
#45 = Utf8 append
#46 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#47 = Utf8 toString
#48 = Utf8 ()Ljava/lang/String;
#49 = Utf8 java/io/PrintStream
#50 = Utf8 println
#51 = Utf8 (Ljava/lang/String;)V
常量池表constant_pool
常量池是一种表结构,它包含了Class文件结构及其及其子结构中引用的所有字符串常量,类或者接口名、字段名或者其他常量。常量池中的所有项都具有如下通用格式:
cp_info { u1 tag; //表示cp_info的单字节标记位 u1 info[]; //两个或更多的字节表示这个常量的信息,信息格式由tag的值确定 }
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整形字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类方法的符号引用 |
CONSTANT_InterfaceMehtodref_info | 11 | 接口方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 方法句柄 |
CONSTANT_MethodType_info | 16 | 方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 动态方法调用点 |
这14种常量的结构如下表所示:
CONSTANT_Class_info { u1 tag; //这个值为 CONSTANT_Class (7) u2 name_index;//注意这是一个index,他表示一个索引,引用的是 CONSTANT_UTF8_info }
注意观察 这个CONSTANT_Class_info类型的常量内部结构是由一个tag(CONSTANT_Class(7))和一个name_index组成,name_index中注意这个index,他表示一个索引的,什么的索引呢?CONSTANT_Utf8_info结构的索引,这个结构用来表示一个有效的类或者接口的二进制名称的内部形式。class文件结构中出现的类或者接口名称都是通过全限定形式来表示的,也被称作二进制名称(题外话:全限定类名含义就类似 java.lang包中定义的Object类的完全限定名称为java.lang.Object)。
那我们接着看CONSTANT_Utf8_info结构,他用于表示字符常量的值,他的结构如下所示:
CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; }
CONSTANT_Utf8(1);后面的length指明了bytes[]数组的长度;最后一个bytes[]数组引用了上一个length作为其长度。字符常量采用改进过的UTF-8编码表示。
4. 访问标志
紧接着常量池之后的两个字节表示访问标志,主要是用来标记类或者接口层次的一些属性。目标之定义了16个标志位中的8位,没有使用到的一律为0。 具体标志位如下表:
标志名称 | 标志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_FINAL | 0x0010 | 是否为final类型 |
ACC_SUPER | 0x0020 | 是否允许使用invokespcial字节码指令的新语义,jdk1.0.2之后编译出来的类,此标志都为真 |
ACC_INTERFACE | 0x0200 | 是否为接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型(对接口和抽象类来说,此标志都为真) |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 是否是注解 |
ACC_ENUM | 0x4000 | 是否是枚举 |
包括这个Class是接口还是类,是否定义为public类型,是否定义为abstract类型,如果是类的话是否声明为final.
5. 类索引、父类索引和接口索引集合
在访问标志之后,有3个用来确定一个类的继承关系的数据,按先后顺序分别是:
- 类索引:用于确定类的全限定名
this_class 的值必须是对 constant_pool 表中项目的一个有效索引值。constant_pool 表在这个索引处的项必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口。
由此可见,类索引为11,根据前面得到的常量池,可以知道第11个常量为:
... #11 = Class #40 // com/princeli/JavaTest ... #40 = Utf8 com/princeli/JavaTest ...
上面name_index指向下面CONSTANT_Utf8_info的索引。
- 父类索引:用于确定父类的全限定名
super_class 的值必须为 0 或者是对 constant_pool 表中项目的一个有效索引值。如果它的值不为 0,那 constant_pool 表在这个索引处的项
必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类的直接父类。当前类的直接父类,以及它所有间接父类的 access_flag 中都不能带有 ACC_FINAL 标记。对于接口来说,它的 Class 文件的 super_class 项的值必须是对 constant_pool 表中项目的一个有效索引值。constant_pool 表在这个索引处的
项必须为代表 java.lang.Object 的 CONSTANT_Class_info 类型常量。如果 Class 文件的 super_class 的值为 0,那这个 Class 文件只可能是定义的是
java.lang.Object 类,只有它是唯一没有父类的类。
由此可见,类索引为12,根据前面得到的常量池,可以知道第12个常量为:
... #12 = Class #41 // java/lang/Object ... #41 = Utf8 java/lang/Object ...
类索引this_class、父类索引super_class都是一个u2类型的数据
- 接口索引:用于描述类实现了哪些接口
接口索引集合包含一个u2类型的接口计数项intefaces_count和若干个u2类型的数据集合。
interfaces_count
接口计数器,interfaces_count 的值表示当前类或接口的直接父接口数量。
interfaces[]
接口表,interfaces[]数组中的每个成员的值必须是一个对 constant_pool 表中项目的一个有效索引值,它的长度为 interfaces_count。每个成员 interfaces[i] 必须为 CONSTANT_Class_info 类型常量 ,其中 0 ≤ i <interfaces_count。在 interfaces[]数组中,成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左边的接口。
6. 字段表集合
fields_count
字段计数器,fields_count 的值表示当前 Class 文件 fields[]数组的成员个数。fields[]数组中每一项都是一个 field_info 结构的数据项,它用于表示该类或接口声明的类字段或者实例字段 。
注意::类字段即被声明为 static 的字段,也称为类变量或者类属性,同样,实例字段是指未被声明为static 的字段。由于《Java 虚拟机规范》中,“Variable”和“Attribute”出现频率很高且在大多数场景中具备其他含义,所以译文中统一把“Field”翻译为“字段”,即“类字段”、“实例字段”。
fields[]
字段表,fields[]数组中的每个成员都必须是一个 fields_info 结构的数据项,用于表示当前类或接口中某个字段的完整描述。fields[]数组描述当前类或接口
声明的所有字段,但不包括从父类或父接口继承的部分。
字段表的格式如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | access_flags | 1 | 字段修饰符 |
u2 | name_index | 1 | 字段和方法简单名称在常量池中的引用 |
u2 | descriptor_index | 1 | 字段和方法描述符在常量池中的引用 |
u2 | attributes_count | 1 | 描述字段额外信息属性的个数 |
attribute_info | attributes | attributes_count | 具体描述字段的额外信息属性 |
field { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes [attributes_count]; }
access_flags
字段修饰符与类中的访问标志很类似,用来描述字段的一些属性:
标志名称 | 标志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_PRIVATE | 0x0002 | 是否为private类型 |
ACC_PROTECTED | 0x0004 | 是否为protected类型 |
ACC_STATIC | 0x0008 | 是否为static类型 |
ACC_FINAL | 0x0010 | 是否为final类型 |
ACC_VOLATILE | 0x0040 | 是否volatile类型 |
ACC_TRANSIENT | 0x0080 | 是否transient类型 |
ACC_SYNTHETIC | 0x1000 | 是否由编译器自动产生 |
ACC_ENUM | 0x4000 | 是否enum类型 |
name_index
字段名字,必须指向一个CONSTANT-UTF8类型的结构
descriptor_index
字段描述符,必须指向一个CONSTANT-UTF8类型的结构
描述符用来描述字段的数据类型、方法的参数列表和返回值。其中基本类型字段的描述符用一个大写字母来表示,而对象类型则用字符L加上对象类型的全限定名来表示。具体如下表:
描述符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
S | 基本类型short |
Z | 基本类型boolean |
V | 基本类型void |
L | 对象类型,如Ljava/lang/Object |
对于数组类型,每一个维度都是用一个前置的“[”来描述,如java.lang.String[][]类型的二位数组将被记录为[[java/lang/String;
描述方法时,将按照先参数列表、后返回值的顺序来描述。其中参数列表严格按照参数的顺序放在一组小括号()之内。例如方法java.lang.String.toString()的描述符为()Ljava/lang/String;
attributes_count,attribute_info
attribute字段,用来描述该变量的属性,因为这个变量没有附加属性,所以attributes_count为0,attribute_info为空。
6.方法表集合
methods_count
方法计数器,methods_count 的值表示当前 Class 文件 methods[]数组的成员个数。Methods[]数组中每一项都是一个 method_info 结构的数据项。
methods[]
方法表,methods[]数组中的每个成员都必须是一个 method_info 结构(§4.6)的数据项,用于表示当前类或接口中某个方法的完整描述。如果某个 method_info 结构
的 access_flags 项既没有设置 ACC_NATIVE 标志也没有设置 ACC_ABSTRACT 标志,那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需要引用其它类。method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、实例初始化方法方法和类或接口初始化方法方法。methods[]数组
只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。
方法表的格式如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | access_flags | 1 | 方法修饰符 |
u2 | name_index | 1 | 方法简单名称在常量池中的引用 |
u2 | descriptor_index | 1 | 方法描述符在常量池中的引用 |
u2 | attributes_count | 1 | 描述方法额外信息属性的个数 |
attribute_info | attributes | attributes_count | 具体描述方法的额外信息属性 |
method { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
access_flags
方法访问标志,表示方法的访问权限和基本属性,比如是public 是不是 final 是不是抽象等,其具体访问标志如下:
标志名称 | 标志值 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public类型 |
ACC_PRIVATE | 0x0002 | 是否为final类型 |
ACC_PROTECTED | 0x0004 | 是否为protected类型 |
ACC_STATIC | 0x0008 | 是否为static类型 |
ACC_FINAL | 0x0010 | 是否为final类型 |
ACC_SYNCHRONIZED | 0x0020 | 是否synchronized类型 |
ACC_BRIDGE | 0x0040 | 是否桥接方法 |
ACC_VARARGS | 0x0080 | 是否接收不定参数 |
ACC_NATIVE | 0x0100 | 是否native方法 |
ACC_ABSTRACT | 0x0400 | 是否abstract |
ACC_STRICTFP | 0x0800 | 是否strictfp |
ACC_SYNTHETIC | 0x1000 | 是否由编译器自动产生 |
name_index
方法名字,必须指向一个CONSTANT_UTF8类型的结构
descriptor_index
方法描述符,必须指向一个CONSTANT_UTF8类型的结构
attributes_count,attribute_info
方法的实际代码存放在属性表attribute_info中的“Code”属性中。
7. 属性表集合
ttributes_count
属性计数器,attributes_count 的值表示当前 Class 文件 attributes 表的成员个数。attributes 表中每一项都是一个 attribute_info 结构(§4.7)的数据项。
attributes[]
属性表,attributes 表的每个项的值必须是 attribute_info 结构。在本规范里,Class 文件结构中的 attributes 表的项包括下列定义的属性:
InnerClasses、EnclosingMethod、Synthetic、Signature、SourceFile,SourceDebugExtension、Deprecated、RuntimeVisibleAnnotations、RuntimeInvisibleAnnotations以及BootstrapMethods属性。
对于支持 Class 文件格式版本号为 49.0 或更高的 Java 虚拟机实现,必须正确识别并读取 attributes 表中的 Signature、RuntimeVisibleAnnotations和RuntimeInvisibleAnnotations属性。
对于支持 Class 文件格式版本号为 51.0 或更高的 Java 虚拟机实现,必须正确识别并读取 attributes 表中的BootstrapMethods属性。
本规范要求任一 Java 虚拟机实现可以自动忽略 Class 文件的 attributes 表中的若干(甚至全部)它不可识别的属性项。任何本规范未定义的属性不能影响 Class 文件的语义,只能提供附加的描述信息。
属性表集合用于描述某些场景的专有信息,它一共有21个属性,属性表结构如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | info | attribute_length |
熟悉了这些属性,学习了class文件结构,这时再用javap -v xxx.class命令来反编译一下字节码,看上去就清晰多了。