1. 类文件结构
JVM规范的类文件结构:
字节码对应名称:
1.1 魔数
Class文件的前四个字节称为魔数,用来确定这个文件是否是能被虚拟机接收的Class文件。值为:
ca fe ba be
例如:
0000000 ca fe ba be 00 00 00 34 00 32 0a 00 06 00 15 09
1.2 版本
第五、六个字节为小版本号;
第七、八个字节为主版本号。
对应:
例如:
0000000 ca fe ba be 00 00 00 34 00 32 0a 00 06 00 15 09
1.3 常量池
第九和第十个字节为常量池的项数的16进制数,但是从1开始计数的而非从0开始。
例如:0000000 ca fe ba be 00 00 00 34 00 32 0a 00 06 00 15 09
常量池值对应类型表:value是10进制,需要将类文件由16进制转为10进制。
1.4 访问标识
例如:0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
- 00 21 找下图中的 20 + 1 :表示一个公共类;
- 00 05 去常量池中找第五项,本类的全限定类名;
- 00 06 去常量池中找第六项,父类的全限定类名;
- 00 00 接口的数量为0。
1.5 成员变量
用于标识接口或类内部中声明的变量,不包括方法内部声明的变量。
包括:修饰符、实例变量或类变量、可变性、并发可见性、字段名称等。
例如:0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
其中 00 00 表示没有成员变量。
访问标识符对应:
类型对应:
1.6 方法信息
方法也包括:访问标识、名称索引、描述索引、属性表索引。
例如:0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
其中 00 02 表示本类中有两个方法。
1.7 附加属性
各个属性表之间不要求有严格顺序,不重复名称即可,可以向编译器中写入自己的属性信息,JVM会忽视掉不认识的属性。
2. 字节码指令
Java虚拟机的指令由一个字节长度的、代表着某种特定操作含义的数字(操作码)已经跟随其后的零至多个代表此操作所需参数(操作数)而构成。
Java的字节码由于限制了Java虚拟机操作码的长度为一个字节(0~255),又由于Class文件格式放弃了编译后代码的操作数和长度对齐,也就是JVM处理超过一个字节的数据时,需要在运行时重建出具体数据的结构。放弃了操作数长度对齐,可以省略很多填充和间隔符号,尽可能的获得短小精干的编译代码。
2.1 JavaP工具
JavaP用来反编译Class文件。
代码如下:
/**
* @Description JavaP反编译测试
* @date 2022/3/27 9:15
*/
public class AgaCom {
public static void main(String[] args) {
System.out.println("JavaP");
}
}
使用JavaP反编译:
2.2 运行流程
初始运行代码:
/**
* @Description 字节码运行流程
* @date 2022/3/27 9:30
*/
public class ShowRunStep {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
使用JavaP反编译之后:
Classfile /D:/NewJava/JavaFoundation/java-jvm/target/classes/jm/java/bytecode/ShowRunStep.class
Last modified 2022-3-27; size 629 bytes
MD5 checksum 49deee0c8b09c3878c12cc33b430f985
Compiled from "ShowRunStep.java"
public class jm.java.bytecode.ShowRunStep
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #7.#25 // java/lang/Object."<init>":()V
#2 = Class #26 // java/lang/Short
#3 = Integer 32768
#4 = Fieldref #27.#28 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #29.#30 // java/io/PrintStream.println:(I)V
#6 = Class #31 // jm/java/bytecode/ShowRunStep
#7 = Class #32 // java/lang/Object
#8 = Utf8 <init>
#9 = Utf8 ()V
#10 = Utf8 Code
#11 = Utf8 LineNumberTable
#12 = Utf8 LocalVariableTable
#13 = Utf8 this
#14 = Utf8 Ljm/java/bytecode/ShowRunStep;
#15 = Utf8 main
#16 = Utf8 ([Ljava/lang/String;)V
#17 = Utf8 args
#18 = Utf8 [Ljava/lang/String;
#19 = Utf8 a
#20 = Utf8 I
#21 = Utf8 b
#22 = Utf8 c
#23 = Utf8 SourceFile
#24 = Utf8 ShowRunStep.java
#25 = NameAndType #8:#9 // "<init>":()V
#26 = Utf8 java/lang/Short
#27 = Class #33 // java/lang/System
#28 = NameAndType #34:#35 // out:Ljava/io/PrintStream;
#29 = Class #36 // java/io/PrintStream
#30 = NameAndType #37:#38 // println:(I)V
#31 = Utf8 jm/java/bytecode/ShowRunStep
#32 = Utf8 java/lang/Object
#33 = Utf8 java/lang/System
#34 = Utf8 out
#35 = Utf8 Ljava/io/PrintStream;
#36 = Utf8 java/io/PrintStream
#37 = Utf8 println
#38 = Utf8 (I)V
{
public jm.java.bytecode.ShowRunStep();
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 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Ljm/java/bytecode/ShowRunStep;
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: bipush 10
2: istore_1
3: ldc #3 // int 32768
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
17: return
LineNumberTable:
line 9: 0
line 10: 3
line 11: 6
line 12: 10
line 13: 17
LocalVariableTable:
Start Length Slot Name Signature
0 18 0 args [Ljava/lang/String;
3 15 1 a I
6 12 2 b I
10 8 3 c I
}
SourceFile: "ShowRunStep.java"
2.2.1 将Class文件的常量池放入到运行中常量池
比较小的数会根字节码的指令存放在一起,但是数字范围超过整数最大值就会存储在常量池中。
例如:# 3 = 32768
2.2.2 方法字节码载入方法区
2.2.3 启动主线程,分配栈帧内存
局部变量表的大小和操作数栈的深度对应上方反编译文件中main方法下的:
stack=2, locals=4
,来存储数据和字节码指令。
2.2.4 执行引擎开始执行字节码
bipush 10
:将一个byte压入操作数栈。基于操作数栈的大小,如果长度不够会补齐,长度超过会分多次压入。
istore 1
将操作数栈顶数据弹出,存入局部变量表 solt 1。
ldc #3
从常量池加载 #3 数据到操作数栈。超过整数的值是在编译期间就计算好了。
isotre 2
弹出操作数栈顶数据存入 slot 2
- 执行
iload 1
iload 2
把局部变量slot 1 和 slot 2 的值读取到操作数栈中。
iadd
弹出操作数栈的变量,执行相加,并且把结果存入操作数栈
isotre 3
把操作数栈顶数据弹出放到 slot 3getstatic #4
,把堆中对象的引用放到操作数栈中。
iload 3
把slot 3 中的值读入到操作数栈中。invokevirtual #5
找到常量池总的 #5 ;定位到方法;给方法分配新的栈帧;传递参数,执行新栈帧中的字节码。
执行完毕,弹出栈帧;清除 main 操作数栈内容
return
完成main方法调用,弹出main栈帧,程序结束。
2.3 字节码命令分析
2.3.1 条件判断
byte,short,char都会按照int比较,因为操作数栈是4字节。
比较成立,会使用goto
来跳转到指定行号的字节码。
源代码:
/**
* @Description 判断语句字节码
* @date 2022/3/27 16:50
*/
public class ShowCheat {
public static void main(String[] args) {
int a = 0;
if (a == 0){
a = 20;
}else{
a = 10;
}
}
}
字节码核心部分:
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12
6: bipush 20
8: istore_1
9: goto 15
12: bipush 10
14: istore_1
15: return
如上述字节码,第三行ifne
判断如果成立就跳转到12行,将10加入操作数栈中,在执行完赋值之后就goto
到15行返回,结束方法的调用。不成立就执行第六行,赋值完毕返回,结束方法调用。
2.3.2 循环控制指令
代码:
/**
* @Description 字节码循环控制指令
* @date 2022/3/27 16:58
*/
public class CircControl {
public static void main(String[] args) {
int a = 0;
while (a < 10){
a++;
}
}
}
字节码核心:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
显然循环控制指令并没有超出条件判断的范畴中,如上方字节码的运行过程是:比较变量a与10的大小,如果大于等于直接跳转到14行结束运行,如果不符合条件就执行++命令,然后通过goto
再跳回第二行,重新运行。
2.4 构造方法及方法调用
2.4.1 <cinit>()V
代码:
/**
* @Description 构造方法——cinit
* @date 2022/3/27 17:08
*/
public class Cinit {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
}
字节码核心:
0: bipush 10
2: putstatic #2 // Field i:I
5: bipush 20
7: putstatic #2 // Field i:I
10: bipush 30
12: putstatic #2 // Field i:I
15: return
编译器会按从上自下的顺序,搜集所有的staic静态代码块和静态成员变量的赋值的代码,合并为
<cinit>()V
。
2.4.2 <init>()V
/**
* @Description 构造方法——init
* @date 2022/3/27 17:12
*/
public class Init {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Init(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Init d = new Init("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
public jm.java.bytecode.Init(java.lang.String, int);
descriptor: (Ljava/lang/String;I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String s1
7: putfield #3 // Field a:Ljava/lang/String;
10: aload_0
11: bipush 20
13: putfield #4 // Field b:I
16: aload_0
17: bipush 10
19: putfield #4 // Field b:I
22: aload_0
23: ldc #5 // String s2
25: putfield #3 // Field a:Ljava/lang/String;
28: aload_0
29: aload_1
30: putfield #3 // Field a:Ljava/lang/String;
33: aload_0
34: iload_2
35: putfield #4 // Field b:I
38: return
LineNumberTable:
line 20: 0
line 8: 4
line 11: 10
line 14: 16
line 17: 22
line 21: 28
line 22: 33
line 23: 38
LocalVariableTable:
Start Length Slot Name Signature
0 39 0 this Ljm/java/bytecode/Init;
0 39 1 a Ljava/lang/String;
0 39 2 b I
编译器也会按照从上至下的顺序,将所有的代码块和成员变量赋值的代码放到一块形成新的构造方法,但原始构造方法的代码放在最后。
2.4.3 方法调用
源代码:
/**
* @Description 字节码——方法调用
* @date 2022/3/27 17:22
*/
public class GetMethod {
public GetMethod() { }
private void test1() { }
private final void test2() { }
public void test3() { }
public static void test4() { }
public static void main(String[] args) {
GetMethod d = new GetMethod();
d.test1();
d.test2();
d.test3();
d.test4();
GetMethod.test4();
}
}
字节码核心:
0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return
- new 是创建【对象】,给对象分配堆内存,执行成功会将【对象引用】压入操作数栈。
- dup 是赋值操作数栈栈顶的内容。
- 普通成员方法是由
invokespecial
调用,属于动态绑定,即支持多态。 - 最终方法(final),私有方法(private),构造方法都是由
invokevirtual
指令来调用,属于静态绑定。 - 如果使用【对应引用】调用静态方法,会执行
pop
指令,把【对象引用】弹出。 - 通过
super
调用父类方法,也会执行invokespecial
。