1. 概述
Java诞生之初曾提出过一个著名的宣传口号:"一次编写,到处运行"。Java实现语言无关性的基础就是虚拟机和字节码(.class文件)。Java虚拟机不和包括Java在内的任何语言绑定,它只与"Class"文件所关联。基于这种特性,Java程序无须重新编译便可以在不同操作系统上运行。
Clojure、Groovy、Scala等语言都是运行在Java虚拟机上。下面展示不同语言被不同编译器编译称.class文件最终运行在Java虚拟机上。
2. Class文件结构
根据Java虚拟机规范Class文件通过ClassFile定义,类似于C语言的结构体。class文件由两种基础的数据类型组成。
- 无符号数:属于基本的数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值。
- 表:是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以_info结尾,表用于描述有层次关系的复合结构数据,整个Class文件本质上是一张表。
java
ClassFile {
u4 magic; //Class 文件的标志
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类会可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
通过ClassFile内容,我们可以知道class文件的组成。
下面用一段代码来解释机器是如何识别字节码然后按照上面的规则来定义class类呢?
java
package com.mawkun.elementary.jvm;
public class ClassFileStructure implements ClassFileInterface {
private static final int staticVar = 0;
private int instanceVar = 0;
public int instanceMethod(int param) throws Exception {
return param ++;
}
}
通过javap解析对应class文件生成格式如下:
java
Classfile /D:/workspace/elementary-knowledge/target/classes/com/mawkun/elementary/jvm/ClassFileStructure.class
Last modified 2024-4-14; size 658 bytes
MD5 checksum be42d41ec51f9b3fe12fd88eebbfa613
Compiled from "ClassFileStructure.java"
public class com.mawkun.elementary.jvm.ClassFileStructure implements com.mawkun.elementary.jvm.ClassFileInterface
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#27 // com/mawkun/elementary/jvm/ClassFileStructure.instanceVar:I
#3 = Class #28 // com/mawkun/elementary/jvm/ClassFileStructure
#4 = Class #29 // java/lang/Object
#5 = Class #30 // com/mawkun/elementary/jvm/ClassFileInterface
#6 = Utf8 staticVar
#7 = Utf8 I
#8 = Utf8 ConstantValue
#9 = Integer 0
#10 = Utf8 instanceVar
#11 = Utf8 <init>
#12 = Utf8 ()V
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/mawkun/elementary/jvm/ClassFileStructure;
#18 = Utf8 instanceMethod
#19 = Utf8 (I)I
#20 = Utf8 param
5: iconst_0
6: putfield 6: putfield #2 // Field instanceVar:I
9: return
5: iconst_0
6: putfield #2 // Field instanceVar:I
9: return
LineNumberTable:
line 3: 0
line 3: 0
line 7: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/mawkun/elementary/jvm/ClassFileStructure;
public int instanceMethod(int) throws java.lang.Exception;
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: iload_1
1: iinc 1, 1
4: ireturn
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/mawkun/elementary/jvm/ClassFileStructure;
0 5 1 param I
Exceptions:
throws java.lang.Exception
MethodParameters:
Name Flags
param
}
SourceFile: "ClassFileStructure.java"
通过编译后生成的class文件格式如下,因为class文件是以8位作为一个字节的二进制流。
2.1 魔数(Magic Number)
arduino
u4 magic; //Class 文件的标志
每个 Class 文件的头 4 个字节称为魔数(Magic Number)。前四个字节是固定值cafebabe,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件。
2.2 Class文件版本号(Major Version & Minor Version)
arduino
u2 minor_version;//Class 的小版本号
u2 major_version;//Class 的大版本号
紧接着魔数的四个字节存储的是 Class 文件的版本号:第 5 和第 6 位是次版本号 ,第 7 和第 8 位是主版本号。
每当 Java 发布大版本(比如 Java 8,Java9)的时候,主版本号都会加 1。你可以使用 javap -v
命令来快速查看 Class 文件的版本号信息。
高版本的 Java 虚拟机可以执行低版本编译器生成的 Class 文件,但是低版本的 Java 虚拟机不能执行高版本编译器生成的 Class 文件。所以,我们在实际开发的时候要确保开发的的 JDK 版本和生产环境的 JDK 版本保持一致。
2.3 常量池(Constant Pool)
ini
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池
紧接着主次版本号之后的是常量池,常量池的数量是 constant_pool_count-1
(常量池计数器是从 1 开始计数的,将第 0 项常量空出来是有特殊考虑的,索引值为 0 代表"不引用任何一个常量池项" )。
常量池主要存放两大常量:字面量和符号引用。字面量比较接近于 Java 语言层面的的常量概念,如文本字符串、声明为 final 的常量值等。而符号引用则属于编译原理方面的概念。包括下面三类常量:
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
常量池中每一项常量都是一个表,这 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_MothodType_info | 16 | 标志方法类型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
.class文件可以通过 javaap -v class类名 指令来看一下其常量池中的信息(javap -v class类名 -> temp.txt : 将结果输出到temp.txt文件)
2.4 访问标志(Access Flags)
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口,是否为 public 或者 abstrace 类型,如果是类的话是否声明为 final 等等。
类的访问和属性修饰符:
2.5 当前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
ini
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
类索引用于确定这个类的全限定名,父类索引用于确定这个类的父类的全限定名,由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object
之外,所有的 java 类都有父类,因此除了 java.lang.Object
外,所有 Java 类的父类索引都不为 0。
接口索引集合用来描述这个类实现了那些接口,这些被实现的接口将按 implements
(如果这个类本身是接口的话则是extends
) 后的接口顺序从左到右排列在接口索引集合中。
2.6 字段表集合(Fields)
ini
u2 fields_count;//Class 文件的字段的个数
field_info fields[fields_count];//一个类会可以有个字段
字段表(field info)用于描述接口或类声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
field info(字段表)的结构:
-
access_flags: 字段的作用域(
public
,private
,protected
修饰符),是实例变量还是类变量(static
修饰符),可否被序列化(transient 修饰符),可变性(final),可见性(volatile 修饰符,是否强制从主内存读写)。 -
name_index: 对常量池的引用,表示的字段的名称;
-
descriptor_index: 对常量池的引用,表示字段和方法的描述符;
-
attributes_count: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数;
-
attributes[attributes_count]: 存放具体属性具体内容。
上述这些信息中,各个修饰符都是布尔值,要么有某个修饰符,要么没有,很适合使用标志位来表示。而字段叫什么名字、字段被定义为什么数据类型这些都是无法固定的,只能引用常量池中常量来描述。
字段的 access_flag 的取值:
2.7 方法表集合(Methods)
ini
u2 methods_count;//Class 文件的方法的数量
method_info methods[methods_count];//一个类可以有个多个方法
methods_count 表示方法的数量,而 method_info 表示方法表。
Class 文件存储格式中对方法的描述与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。
method_info(方法表的) 结构:
方法表的 access_flag 取值:
注意:因为volatile
修饰符和transient
修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了synchronized
、native
、abstract
等关键字修饰方法,所以也就多了这些关键字对应的标志。
2.8 属性表集合(Attributes)
ini
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
在 Class 文件,字段表,方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。与 Class 文件中其它的数据项目要求的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写 入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
3 字节码指令
・加载和存储指令:
・运算指令
・类型转换指令
・对象创建和访问指令
・操作数栈管理指令
・控制转移指令
・异常处理指令
・同步指令
・方法调用和返回执行