Java诞生之初的口号:“一次编写,到处运行(Write Once, Run Anywhere)”。
各个平台的虚拟机与所有平台都同意使用的程序存储格式——字节码(ByteCode)是构成平台无关性的基石。实现语言无关性的基础人生是虚拟机和字节码存储格式。Java虚拟机不和包括Java在内的如何语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集合符号表以及若干其他辅助信息。基于安全方面的考虑,Java虚拟机规范要求在Class文件使用许多强制性的语法和结构化约束,但任一门功能性语言都可以表示为一个能被Java虚拟机所接受的有效的Class文件。作为一个通用的、机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为语言的产品交付媒介。
Class类文件的结构
任何一个Class文化都对应着唯一一个类或接口的定义信息,但反过来说,类或接口并不一定都要定义在文件里(譬如类或接口也可以通过类加载器直接生成)。
Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使整个Class文件中存储的内容几乎全部都是程序运行的必要数据,没有空隙存在。当遇到需要占用8位字节以上空间的数据项时,则会按照高位在前的方式分割成若干个8位字节进行存储。
Class文件格式
类的结构由2种数据结构组成:无符号数 和 表
无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节、8个字节个8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-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 |
魔数与Class文件的版本
每个Class文件的头4个字节称为魔数(Magic Number),它的唯一作用就是确定这个文件是否为一个能被虚拟机接受的Class文件。很多文件存储标准中都是用魔数来进行身份识别,譬如图片格式,如gif或者jpeg等文件头中都存在有魔数。使用魔数而不是扩展名来进行识别主要是基于安全方面考虑,因为文件扩展名可以随意地改动。文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过又不会引起混淆即可。Class文件的魔数的获得很有“浪漫七夕”,值为:0xCAFEBABE(“咖啡宝贝”),这个魔数值在Java还称为做“Oak”语言的时候就确定下来了(这个就是Java图案是一杯咖啡的原因?)
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK1.1之后的每一个JDK大版本发布主版本号上加1(JDK1.0~1.1使用了45.0 ~ 45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,即使文件格式未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
常量池
紧接着主次版本号之后的就是常量池入口,常量池也可以理解为Class文件之中的资源仓库,它是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项目之一,同时它还是在Class文件中第一个出现的表类型数据项目。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。如下图所示,常量池容量(偏移地址:0x00000008)为十六进制0x0013,即十进制的19,这就代表常量池中有18项常量,索引值范围为1~18.在Class文件格式规范中与Java中语言习惯不一样的是,这个容量计数是从1开始而不是从0开始的。Class文件结构中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等容量计数斗鱼一般习惯相同,从0开始的。
常量池中主要存放两大类常量:字面(Literal)和符号引用(Symbolic References)。字面量比较接近Java语言层面的常量概念,如文本字符串、声明为final的常量值等。而符号引用则属于编译原理方面的概念,包括下面三类常量。
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符
- 方法的名称和描述符
虚拟机运行时,需要从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。
常量池的项目类型
这14种表都有一个共同的特点,就是表开始的第一位是一个u1类型的标志位(tag)
类型 | 标志(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_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType | 16 | 表示方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
这14种常量类型各均有自己的结构。
后面是0A,对照表的内容就是
0A–00-04–00-0F
就是第4个常量和第15个常量
Oracle公司已经为我们准备好一个专门用于分析Class文件字节码的工具:javap
G:\idea-workspace\richard-demo\jvm\src\main\java\com\vma\demo>javap -verbose Tes
tClass
警告: 二进制文件TestClass包含com.vma.demo.TestClass
Classfile /G:/idea-workspace/richard-demo/jvm/src/main/java/com/vma/demo/TestCla
ss.class
Last modified 2019-4-25; size 288 bytes
MD5 checksum 7bf673753aefb354bc368bae2de74f45
Compiled from "TestClass.java"
public class com.vma.demo.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/vma/demo/TestClass.m:I
#3 = Class #17 // com/vma/demo/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/vma/demo/TestClass
#18 = Utf8 java/lang/Object
{
public com.vma.demo.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>
":()V
4: return
LineNumberTable:
line 11: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 16: 0
}
SourceFile: "TestClass.java"
从清单可以看出已经把常量池中18项常量都计算出来
14种常量项的结构总表——太多了不想一个个慢慢打,想看自行谷歌百度什么的。
访问标志
常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括这个Class是类还是接口:是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final类等。
public class com.vma.demo.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
0x0021转化为2进制为:0000000000100001
引入图片(这人博客写的贼详细)
根据信息可以判断为public和super吻合。
ACC_PUBLIC,ACC_SUPER
访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否为public型 |
ACC_FINAL | 0x0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0x0020 | 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK1.0.2之后编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 表示这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或抽象类来说,此标志值为真,其他类值为假 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注解 |
ACC_ENUM | 0x4000 | 标识这是一个枚举 |
看完以后,我觉得可以分为5种类型:类、抽象类、接口、注解、枚举。(纯属个人看法误信,希望有大神矫正)
类索引、父类索引与接口索引集合
类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引集合(interfaces)是一组u2类型的数据的集合,Class文件中由这三项数据来确定这个类的继承关系。类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于Java语言不允许多重继承,所以父类索引只有一个,除了Java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口按照implement语句(如果这个类本身是一个接口,则应当是extends语句)后的接口顺讯从左到右排列在接口索引集合中。
先查看一下二进制的数值:
值为0x0030040000
在常量表可以看出,#3#4都对应着CLass,接口索引集合为0000说明没有接口
字段表集合
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括方法内部声明的局部变量。在Java中描述一个字段可以包括的信息有:字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。上述这些信息中,各个修饰符都是布尔值,要么有修饰符,要么没有,很适合使用标志位来表述。而字段叫什么名字、字段被定义为什么数据类型,这些都是无法固定的,只能引用常量池的常量来描述。
图片来源
字段表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
字段访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_PRIVATE | 0x0002 | 字段是否private |
ACC_PROTECTED | 0x0003 | 字段是否protected |
ACC_STATIC | 0x0008 | 字段是否static |
ACC_FINAL | 0x0010 | 字段是否final |
ACC_VOLATILE | 0x0040 | 字段是否volatitle |
ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动生成 |
ACC_ENUM | 0x4000 | 字段是否enum |
描述符标识字符含义
标识字符 | 含义 |
---|---|
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 [][] ”,一个整型数组"int[]"将被记录为“[i”。
用描述符来描述方法时,按照县参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“()”之内。如方法void inc()的描述符为“()V”,方法java.lang.String toString()的描述符为“()Ljava/lang/String”,方法int indexOf(char[] source, int sourceOffsent, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述符为“([CII[CII)I”。
00 01: fields_count
00 02:access_flage
00 05:name_index
00 06:descriptor_index
方法表集合
方法表的结果如同字段表一样,一次包括了访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合(attributes)几项。
方法表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flag | 1 |
u2 | name_index | 1 |
u2 | descriptor_index | 1 |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATITLE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、stricftp和abstract关键字可以修饰方法,所有方法表的访问标志中增加了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志。
方法访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
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 | 方法是否是由编译器自动生成的 |
与字段表集合相对应的,如果父类方法在子类中没有被重写(Override),方法表集合中不会出现来自父类的信息。但同样的,有可能会出现由编译器自动添加的方法,最典型的便是类构造器“”方法和实例构造器“”方法。
在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要必须拥有一个与原方法不同的特征签名(在Java代码中的方法特征签名只包括了方法名称、参数顺序以参数类型,而字节码的特征签名还包括方法返回值以及受查异常表),特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包括在特征签名中,因此Java语言里面是无法仅仅依靠返回值的不同对一个已有方法进行重载。
属性表集合
对于每一个属性,它的名称需要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结果这是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的为数即可。
属性表结构
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u1 | info | attribute_length |
Code属性
Java程序方法体中的代码经过Javac编译器处理后,最终变为字节码指令存储在Code属性内。Code属性出现在方法表的属性集合之中,但并非所有的方法表都必须存在这个属性,譬如接口或者抽象类中方法就不存在Code属性,如果方法表中有Code属性存在。
Code属性表的结构
类型 | 名称 | 属性 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | max_stack | 1 |
u2 | max_locals | 1 |
u4 | code_length | 1 |
u1 | code | code_length |
u2 | exception_table_length | 1 |
exception_info | exception_table | exception_table_length |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
attribute_name_index是一项指向CONSTANT_Utf8_info型常量的索引,常量值固定为“Code”,它代表一个该属性的属性名称,attribute_length指示了属性值的长度,由于属性名称索引与属性长度一共为6字节,所以属性值的长度固定为整个属性表长度减去6。
max_stack代表了操作数栈(Operand Stacks)深度最大值。在方法执行的任意时刻,操作数栈都不会超过整个深度。虚拟机运行的时候需要根据这个值来分配栈帧(Stack Frame)中的操作栈深度。
max_locals代表了局部变量表所需的存储空间,存储单位为Slot。Slot是虚拟机为局部变量分配内存所使用的最小单位。对于Byte、char、float、int、short、boolean、returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要亮哥Slot来存放。方法参数(包括实例方法中的隐藏参数“this”)、显示异常处理器的参数(Exception Handler Parameter,就是try-catch语句中catch块所定义的异常)、方法体重定义的局部变量都需要使用局部变量表来存放。另外不是方法中要用到但是局部变量,就把这些局部变量所占Slot纸盒为max_locals的值,原因是局部变量表中Slot可重用,当代码执行超出一个局部变量的作用域,这个局部变量所占的Slot可以被其他局部变量所使用,Javac编译器会根据变量的作用域来分配Slot给谷歌变量使用,然后计算出max_locals的大小。
code_length和code用来存储Java源程序编译后生成的字节码指令。code_length代表字节码长度,code是用于存储字节码指令的一系列字节流。每个指令就是一个u1类型的单字节,当虚拟机读取到code中的一个字节码是,就可以对应找出这个字节码代表的是什么指令,并且可以自动这个指令后面是否需要跟随参数,以及参数应当如何理解。u1数据类型的取值范围为0x00~0xFF,对应0到255,也就是256条指令,目前Java虚拟机以及有200退编码值对应的指令含义。
code_length,虽然是一个u4类型的长度值,理论上最大值可以达到2^32-1,但是根据Java规范,实际只能使用u2长度,超过这个长度Java编译器会拒绝编译。
Code属性是Class文件中最重要的一个属性,如果把一个Java程序中的信息分为代码(Code,方法体里面的Java代码)和元数据(Metadata,包括类、字段、方法定义以及其他信息)两部分,那么正规Class文件中,Code属性用于描述代码,所有的气压数据项目都用于描述元数据。
在javap中输出的“Args_size”的值为1,在类中的2个方法——实例构造器 < inti >()和inc(),这个两个方法很明显都是没有参数的,但是在方法体或者参数列表里都没有定义局部变量。**在任何实例方法里面,都可以通过“this”关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却非常简单,仅仅是通过Javac编译器编译的时候把对this关键字的访问转变为堆一个普通方法参数的访问,然后在虚拟机调用实例方法是自动传入此参数而。因此在实例方法的局部变量表中至少一会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数才从1开始计算。
异常表
字节码指令之后的是这个方法的显式异常处理,异常表对于Code属性来说并不是必须存在的。
类型 | 名称 | 数量 |
---|---|---|
u2 | start_pc | 1 |
u2 | end_pc | 1 |
u2 | handler_pc | 1 |
u2 | catch_type | 1 |
含义是:当字节码在第start_pc行到第end_pc行之间(不包含第end_pc)出现了类型为catch_type或者其子类的异常(catch_type为指向一个CONSTANT_Class_info型常量的索引),则转到第handler_pc行继续处理。catch_tyoe的值为0时,代表任意异常情况都需要转向到handler_pc处进行处理。
public class DemoTest {
public int inc() {
int x;
try {
x = 1;
return x;
} catch (Exception e) {
x = 2;
return x;
} finally {
x = 3;
}
}
}
{
public com.vma.demo.DemoTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>
":()V
4: return
LineNumberTable:
line 11: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=5, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: istore_2
4: iconst_3
5: istore_1
6: iload_2
7: ireturn
8: astore_2
9: iconst_2
10: istore_1
11: iload_1
12: istore_3
13: iconst_3
14: istore_1
15: iload_3
16: ireturn
17: astore 4
19: iconst_3
20: istore_1
21: aload 4
23: athrow
Exception table:
from to target type
0 4 8 Class java/lang/Exception
0 4 17 any
8 13 17 any
17 19 17 any
LineNumberTable:
line 16: 0
line 17: 2
line 22: 4
line 17: 6
line 18: 8
line 19: 9
line 20: 11
line 22: 13
line 20: 15
line 22: 17
StackMapTable: number_of_entries = 2
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Exception ]
frame_type = 72 /* same_locals_1_stack_item */
stack = [ class java/lang/Throwable ]
}
SourceFile: "DemoTest.java"
编译器为这段Java源码生成了3条异常表记录,对应3条可能出现的代码执行路径。从Java代码的语义上讲,这3条执行路径分别为:
Exceptions属性
属性表
类型 | 名称 | 数量 |
---|---|---|
u2 | attribute_name_index | 1 |
u4 | attribute_length | 1 |
u2 | number_of_exceptions | 1 |
u2 | exception_index_table | number_of_exceptions |
Exceptions属性中的number_of_exceptions项表示方法可能抛出number_of_exceptions种受查异常,每一种受查异常使用一个exception_index_table项表示,exception_index_table是一个指向常量池中CONSTANT_Class_info型常量的索引,代表了该受查异常的类型。
LineNumberTable
LineNumberTable属性