文章目录
-
- 字节码
-
- [JVM 与字节码](#JVM 与字节码)
- 字节码的生成过程
- [Class 文件结构](#Class 文件结构)
-
- [魔数(Magic Number)](#魔数(Magic Number))
- [Class 文件版本号(Minor&Major Version)](#Class 文件版本号(Minor&Major Version))
- [常量池(Constant Pool)](#常量池(Constant Pool))
- [访问标志(Access Flags)](#访问标志(Access Flags))
- [前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合](#前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合)
- 字段表集合(Fields)
- 方法表集合(Methods)
- 属性表集合(Attributes)
- [索引值定位 class 文件中的位置](#索引值定位 class 文件中的位置)
字节码
字节码是一种中间表示形式,它是由Java编译器将Java源代码编译得到的。字节码文件通常以 .class
为扩展名,每个Java类都会生成一个对应的字节码文件。字节码是一种低级的指令集合,它由一系列的指令组成,这些指令是为 JVM 设计的,而不是为特定的硬件平台设计的。
JVM 与字节码
在 Java 中,JVM 可以理解的代码是字节码
(即扩展名为 .class
的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
Groovy、Scala、JRuby、Kotlin 等语言都是运行在 Java 虚拟机之上。不同的语言被不同的编译器编译成 .class
文件最终运行在 Java 虚拟机之上。.class
文件的二进制格式可以使用 WinHexopen in new window 查看。
字节码的生成过程
当Java源代码被编译时,编译器会生成字节码文件。这个过程可以分为以下几个步骤:
- 词法分析:将源代码分解成一个个有意义的记号(Token)。
- 语法分析:将记号组合成符合 Java 语言语法的抽象语法树(AST)。
- 语义分析:检查语法树中的语义错误,如类型不匹配等。
- 代码生成:将语法树转换为字节码指令序列。
Class 文件结构
.class
文件是不同的语言在 Java 虚拟机之间的重要桥梁,同时也是支持 Java 跨平台很重要的一个原因。根据 Java 虚拟机规范,Class 文件通过 ClassFile
定义,有点类似 C 语言的结构体。
ClassFile
的结构如下:
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; //字段数量
field_info fields[fields_count]; //一个类可以有多个字段
u2 methods_count; //方法数量
method_info methods[methods_count]; //一个类可以有个多个方法
u2 attributes_count; //此类的属性表中的属性数
attribute_info attributes[attributes_count]; //属性表集合
}
类文件的内容通常可以分为以下的部分:
通过 IDEA 插件 jclasslib
不光可以直观地查看某个类对应的字节码文件,还可以查看类的基本信息、常量池、接口、属性、函数等信息。
魔数(Magic Number)
Java
u4 magic; // Class 文件的标志
每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 Class 文件 。Java 规范规定魔数为固定值:0xCAFEBABE
。
JVM 会在验证阶段检查 class 文件是否以该魔数开头,如果读取的文件不是以这个魔数开头,Java 虚拟机将拒绝加载它,并会抛出 ClassFormatError
。
Class 文件版本号(Minor&Major Version)
java
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 版本保持一致。
常量池(Constant Pool)
java
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_MethodType_info | 16 | 标志方法类型 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
.class 文件可以通过 javap -v class类名 指令来看一下其常量池中的信息(javap -v class类名-> temp.txt :将结果输出到 temp.txt 文件)。 |
访问标志(Access Flags)
java
u2 access_flags; // Class 的访问标记
在常量池结束之后,紧接着的两个字节代表访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口,是否为 public
或者 abstract
类型,如果是类的话是否声明为 final
等等。
总共有 16 个标记位可供使用,但常用的只有其中 7 个:
标识符 | 标记位 | 描述 |
---|---|---|
ACC_PUBLIC | 0x0001 | 声明为公共的;可以从其包之外访问 |
ACC_FINAL | 0x0010 | 声明为最终的;不允许有子类 |
ACC_SUPER | 0x0020 | 在通过 invokespecial 指令调用时,特别处理超类方法 |
ACC_INTERFACE | 0x0200 | 是一个接口,不是一个类 |
ACC_ABSTRACT | 0x0400 | 声明为抽象的;不能实例化 |
ACC_SYNTHETIC | 0x1000 | 声明为合成的;不在源代码中存在 |
ACC_ANNOTATION | 0x2000 | 声明为注解类型 |
ACC_ENUM | 0x4000 | 声明为枚举类型 |
ACC_MODULE | 0x8000 | 声明为模块类型 |
通过 javap -v class类名
指令可以查看指定类的访问标志。
前类(This Class)、父类(Super Class)、接口(Interfaces)索引集合
java
u2 this_class; // 当前类
u2 super_class; // 父类
u2 interfaces_count; // 接口数量
u2 interfaces[interfaces_count]; // 一个类可以实现多个接口
Java 类的继承关系由类索引、父类索引和接口索引集合三项确定。类索引、父类索引和接口索引集合按照顺序排在访问标志之后,
类索引(this_class)用于确定这个类的全限定名(当前类的索引)。
父类索引(super_class)用于确定这个类的父类的全限定名(父类的索引)。
由于 Java 语言的单继承,所以父类索引只有一个,除了 java.lang.Object
之外,所有的 Java 类都有父类,因此除了 java.lang.Object
外,所有 Java 类的父类索引都不为 0。
接口索引 集合用来描述这个类实现了哪些接口,这些被实现的接口将按 implements
(如果这个类本身是接口的话则是 extends
) 后的接口顺序从左到右排列在接口索引集合中。
字段表集合(Fields)
java
u2 fields_count; // 字段数量
field_info fields[fields_count]; // 一个类会可以有个字段
字段表(field info)用于描述接口或类中声明的变量。字段包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
field info(字段表) 的结构:
java
field_info {
u2 access_flag;
u2 name_index;
u2 description_index;
u2 attributes_count;
}
-
access_flag
:字段的作用域(public
,private
,protected
修饰符),是实例变量还是类变量(static
修饰符),可否被序列化(transient
修饰符),可变性(final
),可见性(volatile
修饰符,是否强制从主内存读写)。 -
name_index
:字段名的索引,指向常量池中的CONSTANT_Utf8_info
, 比如说下面示例中的值就为 name。 -
description_index
:字段的描述类型索引,也指向常量池中的CONSTANT_Utf8_info
,针对不同的数据类型,会有不同规则的描述信息。 -
attributes_count
: 一个字段还会拥有一些额外的属性,attributes_count 存放属性的个数; -
attributes [attributes_count]
: 存放具体属性具体内容。
如
java
public class FieldsExample {
private String name;
}
这段代码的字段只有一个,修饰符为 private
,类型为 String
,字段名为 name
。
方法表集合(Methods)
java
u2 methods_count; // 方法数量
method_info methods[methods_count]; // 一个类可以有个多个方法
methods_count
表示方法的数量,而 method_info
表示方法表。
Class 文件存储格式中对方法的描述,与对字段的描述几乎采用了完全一致的方式。方法表的结构如同字段表一样,依次包括了访问标志 、名称索引 、描述符索引 、属性表集合几项。二者的区别是用来存储方法的信息,包括方法名,方法的参数,方法的签名。
method_info(方法表的) 结构:
标识符 | 标记位 | 描述 |
---|---|---|
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 ;在 Java 之外的其他语言中实现。 |
ACC_ABSTRACT | 0x0400 | 声明为 abstract ;没有提供实现。 |
ACC_STRICT | 0x0800 | 声明为 strictfp ;浮点模式为 FP-strict。 |
ACC_SYNTHETIC | 0x1000 | 声明为 synthetic ;不在源代码中存在。 |
因为 volatile
修饰符和 transient
修饰符不可以修饰方法,所以方法表的访问标志中没有这两个对应的标志,但是增加了 synchronized
、native
、abstract
等关键字修饰方法,所以也就多了这些关键字对应的标志
属性表集合(Attributes)
java
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
在 Class 文件中,字段表和方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格的顺序,并且只要不与已有的属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
以下是包含属性表在内的完整的表示字段的结构
索引值定位 class 文件中的位置
通过索引值,可以定位到在 class 文件中的位置。
在 Java 类文件中,常量池是一个索引表 ,它从索引值 1 开始计数,每个条目都有一个唯一的索引。
-
常量池计数器:在常量池之前,类文件有一个 16位 的常量池计数器,表示常量池中有多少项。它的值比实际常量数大1(因为索引从1开始)。
-
常量池条目:每个常量池条目的开始是一个标签(1个字节),表明了常量的类型(如Class、Fieldref、Methodref等)。根据这个类型,后面跟着的数据结构也不同。
定位过程大致如下:
-
读取常量池计数器:首先,从类文件的开头读取常量池计数器的值,确定常量池中有多少条目。
-
遍历常量池:从常量池的第一项开始遍历。由于不同类型的常量长度不同,需要根据每个常量的类型来确定它的长度。
-
根据索引定位:继续遍历,直到到达所需的索引值。每次遍历时,根据条目类型读取相应长度的数据,直到达到目标索引。