Java字节码初窥
- 一、字节码是什么?
- 二、字节码结构
- 2.1 示例
- 2.2 class整体结构
- 2.2.1 魔数
- 2.2.2 JDK版本号
- 2.2.3 常量池
- 2.2.4 访问标记
- 2.2.5 当前类名
- 2.2.6 父类名称
- 2.2.7 接口
- 2.2.8 字段表
- 2.2.9 方法表
- 2.2.10 附加属性
一、字节码是什么?
“一次编写,到处运行”的口号,是Java拥有目前如此庞大的用户群的一个基石。其中的原因是:
- JVM针对各种操作系统进行定制,屏蔽了系统差异;
- Java编译之后生成字节码,部署到各种系统上面,由JVM加载字节码(.class文件),再通过JIT技术,生成机器码,进行运行。
因此可以看出字节码在Java技术体系的重要地位, 下图是一个.java文件从编写源码到运行的示例图:
Java之所以被称之为字节码,是因为字节码文件由十六进制值组成,而 JVM 以两个十六进制值为一组,即以字节为单位进行读取。对于Java程序员来说,Java字节码是JVM的指令集,而JVM是Java代码运行必须依赖的环境,另外字节码技术在Spring AOP、各种ORM框架、APM中应用屡见不鲜,因此了解字节码及其工作原理,对于编写高性能代码至关重要,深入分析和排查问题也有一定作用。
二、字节码结构
2.1 示例
.java文件通过javac编译后得到一个.class文件,如下图所示的hello类,和生成的class文件:
public class Hello {
public static void main(String[] args) {
int a = 1;
int b = 2;
int sum = a + b;
}
}
通过javac编译,一般采用如下的命令:
javac -g src/main/java/com/johar/jeektime/jvmweek1/topic1/Hello.java -d target/classes
- -d:指定生成class文件路径,若不指定-d,默认在当前目录
- -g:由于javac工具默认开启优化功能,生成的字节码中没有局部变量表,使用-g可以关闭优化功能,生成所有调试信息
下图是使用notepad++打开后的class文件: - 使用javap反编译class文件,一般命令如下:
javap -c -v src/main/java/com/johar/jeektime/jvmweek1/topic1/Hello.class
- -c: 对代码进行反汇编
- -v: 输出附件信息
Classfile /E:/JavaTrain/homework/JAVA-000/Week_01/jvm-week1/src/main/java/com/johar/jeektime/jvmweek1/topic1/hello.class
Last modified 2021-2-25; size 512 bytes
MD5 checksum a12d379d9ed8fc58b46e8717610eabe8
Compiled from "Hello.java"
public class com.johar.jeektime.jvmweek1.topic1.Hello
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #3.#22 // java/lang/Object."<init>":()V
#2 = Class #23 // com/johar/jeektime/jvmweek1/topic1/Hello
#3 = Class #24 // java/lang/Object
#4 = Utf8 count
#5 = Utf8 I
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LineNumberTable
#10 = Utf8 LocalVariableTable
#11 = Utf8 this
#12 = Utf8 Lcom/johar/jeektime/jvmweek1/topic1/Hello;
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 args
#16 = Utf8 [Ljava/lang/String;
#17 = Utf8 a
#18 = Utf8 b
#19 = Utf8 sum
#20 = Utf8 SourceFile
#21 = Utf8 Hello.java
#22 = NameAndType #6:#7 // "<init>":()V
#23 = Utf8 com/johar/jeektime/jvmweek1/topic1/Hello
#24 = Utf8 java/lang/Object
{
public com.johar.jeektime.jvmweek1.topic1.Hello();
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 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/johar/jeektime/jvmweek1/topic1/Hello;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=4, args_size=1
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: return
LineNumberTable:
line 15: 0
line 16: 2
line 17: 4
line 35: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
2 7 1 a I
4 5 2 b I
8 1 3 sum I
}
SourceFile: "Hello.java"
2.2 class整体结构
JVM规范要求每个字节码文件都要按照如下图格式组成,后面会通过一个实例进行讲解。
2.2.1 魔数
Magic魔数的唯一作用就是确定这个文件是否为一个能被虚拟机所接受的class文件。魔数值固定为0xcafebabe,不回改变,可以对照示例的class文件看一下。
2.2.2 JDK版本号
版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。示例class文件中的版本号为“00 00 00 34”,转换成10进制为52.0,通过查询序号,可以看到jdk的版本为1.8.0.
对应javap反编译的bytecode如下,可以方便看到jdk的版本:
minor version: 0
major version: 52
2.2.3 常量池
jdk主版本号之后的字节为常量池,常量池中存储两类常量:字面量与符号引用。字面量为代码中申明为final 的常量值,符号引用如类和接口的全局限定名、字段名称和描述符、方法的名称和描述符。常量池的代码结构如下:
如上图所示,常量池分为量部分,常量池计数器,和常量数据区。
- 常量池计数器
常量池计数器的值等于常量池表中成员数+1。 - 常量池数数据区
常量池可以理解为一个常量的字典,使用编号的方式管理程序中的各种常量。常量池表的所有项格式如下:
cp_info {
u1 tag;
u1 info[];
}
在常量池表中,每个cp_info项都必须以一个表示cp_info类型的单字节"tag"项开头。后面info[]数组的内容由tag的值所决定。tag对应的类型如下表所示:
通过javap反编译之后的bytecode显示如下:
Constant pool:
#1 = Methodref #3.#12 // java/lang/Object."<init>":()V
#2 = Class #13 // com/johar/jeektime/jvmweek1/topic1/Hello
#3 = Class #14 // java/lang/Object
#4 = Utf8 <init>
#5 = Utf8 ()V
#6 = Utf8 Code
#7 = Utf8 LineNumberTable
#8 = Utf8 main
#9 = Utf8 ([Ljava/lang/String;)V
#10 = Utf8 SourceFile
#11 = Utf8 Hello.java
#12 = NameAndType #4:#5 // "<init>":()V
#13 = Utf8 com/johar/jeektime/jvmweek1/topic1/Hello
#14 = Utf8 java/lang/Object
以第一行为例:
- #1 常量编号,其他使用的地方直接引用常量编号
- = 分隔符
- Methodref 表明这个常量指向一个方法,具体的就是指向 #3 (Object)的 #12 方法(""😦)V),具体的方法以及在双斜杠后面显示
2.2.4 访问标记
常量池之后的两个字节是访问标记,描述该类或者接口的访问权限及属性,具体如下所示:
实例中的ACC_PUBLIC表示是public, ACC_SUPER则是历史原因,JDK1.0引入ACC_SUPER标记修正invokespecial指令调用super类的方法的问题。
2.2.5 当前类名
访问标记的两个字节是当前类的全限定名,引用的是常量池中的索引值。
2.2.6 父类名称
当前类名后的两个字节是父类名称,引用的是常量池中的索引值。
2.2.7 接口
父类名称之后的两个字节是接口计数器,表示该类实现的接口数量。其后的n个字节是所有接口名称的字符串常量的引用值。
2.2.8 字段表
字段表用于描述类和接口中申明的变量,包含类级别的变量和实例变量,由字段个数和每个字段详细信息组成。
2.2.9 方法表
方法表描述的是方法的访问标记、方法名、方法描述符以及方法的属性,如下图所示:
javap反编译后的实例为:
public com.johar.jeektime.jvmweek1.topic1.Hello(); // 默认构造方法
descriptor: ()V // 方法描述,没有入参,返回值为void
flags: ACC_PUBLIC // 访问标记为public
Code:
stack=1, locals=1, args_size=1 // 栈深度为1,局部变量表为1,方法入参个数为1,默认的this
0: aload_0 // 将this从局部变量表压入操作数栈
1: invokespecial #1 // 调用hello父类的初始化方法 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 10: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/johar/jeektime/jvmweek1/topic1/Hello;
public static void main(java.lang.String[]); // main方法
descriptor: ([Ljava/lang/String;)V // 方法描述符,[表示数组, L表示对象,java/lang/String表示入参来下,V表示返回值为void
flags: ACC_PUBLIC, ACC_STATIC // 访问标记为public static
Code:
stack=2, locals=4, args_size=1
0: iconst_1 // 将常量1加入到栈顶
1: istore_1 // 将栈顶常量1存储到LocalVariableTable Slot1,并栈顶删除
2: iconst_2 // 将常量2加入到栈
3: istore_2 // 将栈顶常量2存储到LocalVariableTable Slot2,并从栈顶删除
4: iload_1 // 将LocalVariableTable Slot1 压入到栈
5: iload_2 // LocalVariableTable Slot2 压入到栈
6: iadd // 栈顶两个数相加,结果放在栈顶
7: istore_3 // 将栈顶的3存储到LocalVariableTable Slot3,并从栈顶删除
8: return
LineNumberTable:
line 15: 0
line 16: 2
line 17: 4
line 35: 8
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 args [Ljava/lang/String;
2 7 1 a I
4 5 2 b I
8 1 3 sum I