文章目录
- 一、JVM字节码概述
- 一、文件结构概述
- 二、详细解析
- 1. 魔数和Class文件的版本
- 2. 常量池
- 3. 访问标志
- 4. 类索引、父类索引与接口索引集合
- 5. 字段表和方法表
- 6. 属性表
- 字节码
- Spotbugs
作为一名资深的Java开发工程师,对JVM及其字节码有着深入的理解。现在,我将为初级Java开发工程师详细介绍JVM的字节码。
一、JVM字节码概述
Java字节码(Java Bytecode)是Java虚拟机(JVM)执行的一种指令格式。它是Java源代码经过Java编译器(javac)编译后生成的中间代码,存储在.class文件中。这种中间代码不依赖于任何具体的硬件或操作系统,而是由JVM进行解释执行或即时编译(JIT编译)成机器码后执行,从而实现“一次编写,到处运行”的跨平台特性。
一、文件结构概述
Class文件是一组以8位字节为基础单位的二进制流,其中包含了Java虚拟机(JVM)执行程序所需的各种信息。Class文件结构紧凑,数据项目之间严格按照顺序排列,中间没有空隙。这些数据项目通过无符号数和表来存储,主要包括以下几个部分:
-
魔数和Class文件的版本:文件开头是魔数(magic number),用于标识这是一个Class文件,紧接着是Class文件的版本号,包括主版本号和次版本号。
-
常量池:常量池是Class文件的资源仓库,用于存放编译期生成的各种字面量和符号引用。常量池中的常量数量不固定,因此在常量池入口需要放置一个u2类型的数据来表示常量池容量计数值(从1开始计数)。
-
访问标志:紧接着常量池的是访问标志,用于识别类或接口的访问信息,如是否为public、abstract、final等。
-
类索引、父类索引与接口索引集合:这三项数据用于确定类的继承关系,包括类的全限定名、父类的全限定名以及实现的接口列表。
-
字段表:用于描述类或接口中声明的变量,包括类级变量和实例级变量。
-
方法表:方法表的结构类似于字段表,用于描述类中的方法信息。方法的具体实现(字节码指令)存储在方法属性表中的Code属性里。
-
属性表:Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。
二、详细解析
1. 魔数和Class文件的版本
- 魔数:Class文件的开头是4个字节的魔数(CAFEBABE),用于标识这是一个Class文件。
- 版本号:紧接着魔数的4个字节存储的是Class文件的版本号,包括主版本号和次版本号。JDK1.2~JDK12之间次版本号都为0,JDK12以后重新启用了次版本号。
2. 常量池
- 作用:常量池是Class文件的资源仓库,主要存放两大类常量:字面量和符号引用。
- 字面量:包括文本字符串、声明为final的常量值等。
- 符号引用:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符等。
- 编号:常量池中的每个常量都有一个唯一的编号(从1开始),在字段和方法的字节码指令中通过常量编号来引用常量。
3. 访问标志
- 作用:用于识别类或接口的访问信息,如是否为public、abstract、final等。
- 标志值:如ACC_PUBLIC(0x0001)、ACC_FINAL(0x0010)、ACC_INTERFACE(0x0200)等。
4. 类索引、父类索引与接口索引集合
- 类索引:指向常量池的一个索引,用于确定类的全限定名。
- 父类索引:指向常量池的一个索引,用于确定类的父类全限定名。所有Java类(除了java.lang.Object)都有父类,因此父类索引不为0。
- 接口索引集合:一个指向常量池中所有接口全限定名的容器,用于描述类实现的接口列表。
5. 字段表和方法表
- 字段表:用于描述类或接口中声明的变量,包括字段的作用域、是否为static、是否为final、是否为volatile、是否可序列化等信息。
- 方法表:用于描述类中的方法信息,包括方法的访问标志、名称索引、描述符索引等。方法的具体实现(字节码指令)存储在方法属性表中的Code属性里。
6. 属性表
- 作用:Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息。
- 预定义属性:如Code属性(存储方法的字节码指令)、ConstantValue属性(存储final字段的常量值)、Exceptions属性(存储方法抛出的异常列表)等。
字节码
jvm 中的指令是由操作码和其后的操作数构成。字节码使用大端序表示,高位在前,低位在后。
opcode [operand1, operand2]
java虚拟机的常见实现里, Hotspot 是基于栈的,DalvikVM 基于寄存器。对于 Hotspot JVM(不特殊指定,默认)每个线程都有一个虚拟机栈来存储战阵,每次方法调用都伴随栈帧的创建、销毁。
常见的递归方法的栈溢出就是在这里。每个栈帧有自己的局部变量表、操作数栈(Operand Stack)和指向常量池的引用。
1.局部变量表
每个栈帧内部都包含一组称为局部变量表的变量列表,局部变量表的大小在编译期间就已经确定,对应class文件中方法Code属性的maxlocals字段,Java虚拟机会根据maxlocals字段来分配方法执行过程中需要分配的最大的局部变量表容量。
2.操作数栈
每个栈帧内部都包含一个称为操作数栈的后进先出(LIFO)栈,栈的大小同样也是在编译期间确定。Java虚拟机提供的很多字节码指令用于从局部变量表或者对象实例的字段中复制常量或者变量到操作数栈,也有一些指令用于从操作数栈取走数据、操作数据和把操作结果重新入栈。在方法调用时,操作数栈也用于准备调用方法的参数和接收方法返回的结果。
字节码指令
加载(load)和存储(store)相关的指令是使用得最频繁的指令,分为1oad类、store类常量加载这三种。
1)load 类指令是将局部变量表中的变量加载到操作数栈。
2)store 类指令是将栈顶的数据存储到局部变量表中。
3)常量加载相关的指令,常见的有const类、push类、ldc类。const、push 类指令是将常量值直接加载到操作数栈顶。
方法执行:
invokestatic:用于调用静态方法。
invokespecial:用于调用私有实例方法、构造器方法以及使用super 关键字调用父类的实例方法等。
invokevirtual:用于调用非私有实例方法。
invokeinterface:用于调用接口方法。
Spotbugs
上面的基础概念了解之后,下面对应一下
String calledClassName = getClassConstantOperand(); // 类名
String calledMethodName = getNameConstantOperand();// 方法名
String calledMethodSig = getSigConstantOperand();// 方法签名
参考资料:
《/深入理解JVM字节码》