字节码介绍
经过javac
命令编译生成的二进制文件(包括但不限于.java
文件),称为字节码文件,字节码也可能是从网络传输过来的一段二进制流。
一个可运行的(被JVM认可)字节码必须是符合字节码指令规范的,这样才能保证JVM跨平台的特性。
字节码的组成结构
魔数
标识文件类型的依据,通过文件的前四个字节来记录文件的类型。
好比身份证里的性别位,不管
Q:有了文件后缀,为什么还有记录文件类型呢?
A:文件后缀可以被随意更改,png修改为jpg等,而二进制文件修改起来不容易。
在Java的.class文件中,前四个字节存储的分别是,咖啡北鼻(谐音)
0 | 1 | 2 | 3 |
---|---|---|---|
CA | FE | BA | BE |
而其它的常见文件类型存储的头4字节如下:
- JPEG:
FF
D8
FF
- PNG:
89
50
4E
47
- GIF:
47
49
46
38
- TIFF:
49
49
2A
00
文件版本
字节码的第5~6
记录的是次要版本号,7~8
位记录是主要版本号
也就是.class
文件编译时的jdk
版本号,.class
文件记录的是16进制。以下是常见版本号的对应关系
发布版本号 | 内部版本号(十六进制) | 内部版本号(十进制) |
---|---|---|
1.5 | 31 | 49 |
1.6 | 32 | 50 |
1.7 | 33 | 51 |
1.8 | 34 | 52 |
class文件案例如下:
如果class文件的版本号高于运行时jvm
支持的版本号,则会抛出UnSupportedClassVersionError
异常。
常量池
常量池存在的意义是什么?
我认为常量池内部存储了一些已知的字面量信息,像字符串,final数据,提前将这些数据存储好可以避免在程序运行时频繁创建重复对象。提高程序的运行效率。
还有就是常量池以槽位的方式进行存储,实际上被很对地方进行符号引用,这样程序在使用的时候不需要实际创建对象,就能获取到对象。
常量池包含3部分内容:字面量、符号引用、常量池类型数据表
常量池记录了定义这个类所有的信息,比如各个变量的名称,类型,值等信息,类信息,方法信息,字段信息,引用信息等。
访问标志
简单点来讲就是保存:
- 修饰符:
public
、private
、protected
、abstract
、final
修饰符可以修饰类、类属性、方法,所以字节码的访问标志就是保存这些东西,说明了类、字段、方法是否可以访问。
以下是类和字段的所有访问标志信息:
实际上不需要记住所有的标志,了解后有个概念就可以。
表格由
Chat-GPT
生成
访问标志 | 十六进制值 | 说明 |
---|---|---|
ACC_PUBLIC 类 字段 方法 |
0x0001 | 声明为public,可以从任何包访问 |
ACC_PRIVATE 类 字段 方法 |
0x0002 | 声明为private,只能在自己的类中访问 |
ACC_PROTECTED 类 字段 方法 |
0x0004 | 声明为protected,自己、子类及同一个包中类可以访问 |
ACC_STATIC 字段 方法 |
0x0008 | 声明为static,静态字段或方法 |
ACC_FINAL 类 字段 方法 |
0x0010 | 声明为final,不可被继承、覆盖或重新赋值 |
ACC_SUPER 类 |
0x0020 | 允许子类调用父类的方法 |
ACC_SYNCHRONIZED 方法 |
0x0020 | 声明方法为synchronized,线程安全 |
ACC_VOLATILE 字段 |
0x0040 | 声明字段为volatile,保证线程可见性 |
ACC_BRIDGE 方法 |
0x0040 | 指示方法为桥接方法(由编译器生成) |
ACC_TRANSIENT 字段 |
0x0080 | 声明字段为transient,不会被序列化 |
ACC_VARARGS 方法 |
0x0080 | 声明方法接受可变数量的参数 |
ACC_NATIVE 方法 |
0x0100 | 声明方法为native,调用本地C/C++代码 |
ACC_INTERFACE 类 |
0x0200 | 声明为interface,接口类型 |
ACC_ABSTRACT 类 方法 |
0x0400 | 声明为abstract,抽象类或方法 |
ACC_STRICT 方法 |
0x0800 | 声明方法为strictfp,浮点模式严格 |
ACC_SYNTHETIC 类 字段 方法 |
0x1000 | 标记为synthetic,不由源代码直接产生 |
ACC_ANNOTATION 类 |
0x2000 | 声明为annotation,注解类型 |
ACC_ENUM 类 字段 |
0x4000 | 声明为enum,枚举类型 |
类/父类索引与接口索引集合
用于保存和记录类的父类,以及实现的接口等信息。
因为
java
规定,只能单继承,但可以实现多个接口,所以接口会有多个,而父类信息就只会有单个
我们在jclasslib
中的接口
选项中就可以看到具体实现的接口信息:
字段表集合
用于描述接口或类中的字段信息
方法表集合
描述接口或类中的实例(Instance)方法和静态方法。
记录的方法的入参,出参信息。
方法是在类加载阶段就会执行的方法,主要就是对静态的变量进行初始化,他是在编译时自动生成的
属性表
记录的源文件的信息,和类的注解信息
完整的字节码
源代码
package cn.yufire.aqs;
/**
* @author Yu
* @date 2023-01-07 0007 12:03:12
* javap 反编译 栈帧测试
*/
public class ZhanZhen {
// main入栈
public static void main(String[] args) {
// method1() 入栈
System.out.println(method1());
}
public static int method1() {
// 返回给main() method2() 入栈
return method2();
}
public static int method2() {
// 返回给method1() method3() 入栈
return 5 + method3();
}
public static int method3() {
// 局部变量表
int a = 21;
int b = 44;
// 返回给method2()
return a + b;
}
}
源代码对应的字节码
Classfile /D:/WorkSpace/Project/IDEA/Java-Learing/target/classes/cn/yufire/aqs/ZhanZhen.class
Last modified 2023-1-7; size 770 bytes
MD5 checksum d74a90facb1bbfa0c2963dae1626b9f5
Compiled from "ZhanZhen.java"
public class cn.yufire.aqs.ZhanZhen
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool: // 常量池
#1 = Methodref #8.#29 // java/lang/Object."":()V
#2 = Fieldref #30.#31 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #7.#32 // cn/yufire/aqs/ZhanZhen.method1:()I
#4 = Methodref #33.#34 // java/io/PrintStream.println:(I)V
#5 = Methodref #7.#35 // cn/yufire/aqs/ZhanZhen.method2:()I
#6 = Methodref #7.#36 // cn/yufire/aqs/ZhanZhen.method3:()I
#7 = Class #37 // cn/yufire/aqs/ZhanZhen
#8 = Class #38 // java/lang/Object
#9 = Utf8
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Utf8 LineNumberTable
#13 = Utf8 LocalVariableTable
#14 = Utf8 this
#15 = Utf8 Lcn/yufire/aqs/ZhanZhen;
#16 = Utf8 main
#17 = Utf8 ([Ljava/lang/String;)V
#18 = Utf8 args
#19 = Utf8 [Ljava/lang/String;
#20 = Utf8 method1
#21 = Utf8 ()I
#22 = Utf8 method2
#23 = Utf8 method3
#24 = Utf8 a
#25 = Utf8 I
#26 = Utf8 b
#27 = Utf8 SourceFile
#28 = Utf8 ZhanZhen.java
#29 = NameAndType #9:#10 // "":()V
#30 = Class #39 // java/lang/System
#31 = NameAndType #40:#41 // out:Ljava/io/PrintStream;
#32 = NameAndType #20:#21 // method1:()I
#33 = Class #42 // java/io/PrintStream
#34 = NameAndType #43:#44 // println:(I)V
#35 = NameAndType #22:#21 // method2:()I
#36 = NameAndType #23:#21 // method3:()I
#37 = Utf8 cn/yufire/aqs/ZhanZhen
#38 = Utf8 java/lang/Object
#39 = Utf8 java/lang/System
#40 = Utf8 out
#41 = Utf8 Ljava/io/PrintStream;
#42 = Utf8 java/io/PrintStream
#43 = Utf8 println
#44 = Utf8 (I)V
{ // 类信息和方法信息
public cn.yufire.aqs.ZhanZhen();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcn/yufire/aqs/ZhanZhen;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
// 这个地方的#2、#3、#4是动态链接,对应的就是常量池的#2、3、4
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: invokestatic #3 // Method method1:()I
6: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
9: return
LineNumberTable:
line 11: 0
line 12: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
public static int method1();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0 //stack:操作数栈的最大深度、locals:局部变量表的长度、args_size:方法接收参数的个数(this参数)
0: invokestatic #5 // Method method2:()I
3: ireturn
LineNumberTable:
line 16: 0
public static int method2();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=0, args_size=0 //stack:操作数栈的最大深度、locals:局部变量表的长度、args_size:方法接收参数的个数(this参数)
0: iconst_5
1: invokestatic #6 // Method method3:()I
4: iadd
5: ireturn
LineNumberTable:
line 20: 0
public static int method3();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=0 //stack:操作数栈的最大深度、locals:局部变量表的长度、args_size:方法接收参数的个数(this参数)
偏移量:操作码 操作数
0: bipush 21
2: istore_0
3: bipush 44
5: istore_1
6: iload_0
7: iload_1
8: iadd
9: ireturn
//行号表:指明字节码指令的偏移量与Java源代码中代码的行号的一一对应关系
LineNumberTable:
line 24: 0
line 25: 3
line 27: 6
//局部变量表:描述内部局部变量的相关信息
LocalVariableTable:
Start Length Slot Name Signature
3 7 0 a I
6 4 1 b I
}
字节码指令
字节码指令是JVM规定的运行时指令,通俗的来讲就是JVM能看懂的语言,通过这些指令JVM可以得知要进行什么操作,要干什么。有了这些指令代码才能够正常运行。
- 字节码指令与OS无关,由JVM统一处理和执行。
- 字节码指令总数不超过256个。也就是说字节码指令的不会大于256个。
字节码指令格式
使用了两个常见的指令作为案例,实际上指令个数远远不止2个。
invokevirtual
#8
//cp_info#8:Java/lang/StringBuilder.append
invokevirtual
字节码指令#8
符号引用,引用保存在常量池内的#8
号槽位- 拿到引用信息后就可以得知要执行什么方法了
new
#6
//cp_info#6:
new
字节码指令#6
创建一个#6
槽位的引用对象- 也就是创建一个
StringBuilder
对象,自动保存在堆内存中。
实际的字节码信息如下
左侧为代码,右侧为代码对应的字节码指令
字节码指令分类
字节码指令可以分为以下几大类:
附
完整的字节码指令图如下:
高清版下载地址:传送门