Class ByteCode
.java文件通过javac编译后将得到一个.class文件
JVM对于字节码是有规范要求
Magic Number
所有的.class文件的前四个字节都是魔数,固定值为:0xCAFEBABE(咖啡宝贝)
魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件
Version Number
版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)
转化为十进制后可以在Oracle官网进行查询序号对应的JDK版本,进而得知该字节码文件是由哪个版本的JDK编译的
Constant Pool
在字节码中,所有变量和方法都是以符号引用的形式保存在class文件的常量池中。字节码被类加载器加载后,class文件中的常量池会被加载到方法区的运行时常量池,动态链接会将运行时常量池中的符号引用转化为调用方法的直接引用。
(IDEA配合jclasslib Bytecode Viewer
来查看字节码)
以下面这个类为例子
Copy package com . demo . asm ;
public class Test {
private int num = 1 ;
public static int NUM = 100 ;
public int func ( int a , int b) {
return add(a , b) ;
}
public int add ( int a , int b) {
return a + b + num;
}
public int sub ( int a , int b) {
return a - b - NUM;
}
}
javap -c Test.class
javap 是 Java Class文件分解器,可以用于反编译,也可以用于查看字节码
-c
输出类中的所有方法以及字节码信息
Copy public class com.demo.asm.Test {
public static int NUM;
public com.demo.asm.Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field num:I
9: return
public int func(int, int);
Code:
0: aload_0
1: iload_1
2: iload_2
3: invokevirtual #3 // Method add:(II)I
6: ireturn
public int add(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: aload_0
4: getfield #2 // Field num:I
7: iadd
8: ireturn
public int sub(int, int);
Code:
0: iload_1
1: iload_2
2: isub
3: getstatic #4 // Field NUM:I
6: isub
7: ireturn
static {};
Code:
0: bipush 100
2: putstatic #4 // Field NUM:I
5: return
}
主要看一下莫名其妙生成出来的构造方法
左边的数字表示每个指令的偏移量,保存在PC程序计数器中
JVM的栈帧中存储如下内容
动态链接(Dynamic Linking)指向运行时常量池的方法引用
在讲解指令之前先了解一下局部变量表
JVM会为每个方法分配对应的局部变量表。局部变量表也称为局部变量数组或本地变量表,定义为一个数字数组,每个slot存储方法参数和定义在方法体内的局部变量。如果方法为实例方法,则第一个slot为this指针,若是静态方法则没有。
0 aload_0
aload_x 从局部变量表的相应位置x装载一个对象引用到操作数栈的栈顶
aload_0表示把第0个引用类型本地变量(即this指针)推送到操作数栈顶
a代表对象引用
还有其他用于用于装载非对象引用的指令
iload、lload、fload、dload(i=int、l=long、f=float、d=double)
对应aload还有astore,将操作数栈栈顶元素放入局部变量表
1 invokespecial #1
弹栈并执行#1的方法
调用构造函数,这里调用了父类的构造器(#1符号引用指向对应的init方法)
(()V
表示返回void,参数为空)
6 putfield
接受一个操作数,这个操作数引用的是运行时常量池里的一个字段,这里为num(即#2符号引用)。putfield会弹出栈顶两个值,即更新this的num字段为常量1。
可以看出JVM执行字节码的流程和CPU执行机器码的步骤一样,均为取指译码执行
方法调用指令
invokevirtual:调用虚方法(除上面三种情况之外的方法,如调用对象方法)
现在再看下面完整的字节码应该就不成问题了。
javac -g Test.java
(-g
生成局部变量)
javap -verbose Test.class
-verbose 显示详细信息,输出栈大小,方法参数的个数
Copy public class com.demo.asm.Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#26 // java/lang/Object."<init>":()V
#2 = Fieldref #5.#27 // com/demo/asm/Test.num:I
#3 = Methodref #5.#28 // com/demo/asm/Test.add:(II)I
#4 = Fieldref #5.#29 // com/demo/asm/Test.NUM:I
#5 = Class #30 // com/demo/asm/Test
#6 = Class #31 // java/lang/Object
#7 = Utf8 num
#8 = Utf8 I
#9 = Utf8 NUM
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/demo/asm/Test;
#17 = Utf8 func
#18 = Utf8 (II)I
#19 = Utf8 a
#20 = Utf8 b
#21 = Utf8 add
#22 = Utf8 sub
#23 = Utf8 <clinit>
#24 = Utf8 SourceFile
#25 = Utf8 Test.java
#26 = NameAndType #10:#11 // "<init>":()V
#27 = NameAndType #7:#8 // num:I
#28 = NameAndType #21:#18 // add:(II)I
#29 = NameAndType #9:#8 // NUM:I
#30 = Utf8 com/demo/asm/Test
#31 = Utf8 java/lang/Object
{
public static int NUM;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public com.demo.asm.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field num:I
9: return
LineNumberTable:
line 3: 0
line 4: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/demo/asm/Test;
public int func(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=3, locals=3, args_size=3
0: aload_0
1: iload_1
2: iload_2
3: invokevirtual #3 // Method add:(II)I
6: ireturn
LineNumberTable:
line 8: 0
LocalVariableTable:
Start Length Slot Name Signature
0 7 0 this Lcom/demo/asm/Test;
0 7 1 a I
0 7 2 b I
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: iadd
3: aload_0
4: getfield #2 // Field num:I
7: iadd
8: ireturn
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 this Lcom/demo/asm/Test;
0 9 1 a I
0 9 2 b I
public int sub(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=3
0: iload_1
1: iload_2
2: isub
3: getstatic #4 // Field NUM:I
6: isub
7: ireturn
LineNumberTable:
line 16: 0
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 this Lcom/demo/asm/Test;
0 8 1 a I
0 8 2 b I
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: bipush 100
2: putstatic #4 // Field NUM:I
5: return
LineNumberTable:
line 5: 0
}
SourceFile: "Test.java"
Copy java . util . List < String > cmds = new java . util . ArrayList < String >();
new指令后面都会跟着一个dup指令,再跟着一个invokespecial指令,再用astore存储对象引用到局部变量表
new:创建一个对象,并将其引用值入栈
dup:复制栈顶值并将栈顶值入栈
invokespecial:调用实例初始化方法,需要从操作数栈弹出一个this引用
astore:将操作数栈顶元素弹出,放入局部变量表。
可知要把new一个对象并把它的引用放入局部变量表,需要弹栈两次,因此才多了dup指令
CheatSheet
ldc: 将int、float、或者一个类、方法类型或方法句柄的符号引用、String型常量值从常量池中推送至栈顶
bipush: -128~127的int数值常量入栈
Reference
Java 进阶之字节码剖析_CSDN博客
Last updated 9 months ago