Java中的字节码,英文名为bytecode, 是Java代码编译后的中间代码格式。JVM需要读取并解析字节码才能执行相应的任务。
Java字节码是JVM的指令集。JVM加载字节码格式的class文件,校验之后通过JIT编译器转换为本地机器代码执行。
现在市面上通过修改字节码来实现某个功能的技术框架屡见不鲜,例如Mock,AOP,Profile等工具框架,都是基于字节码操作来实现的,所以简单了解一下Java字节码技术还是有必要的。
下面将通过介绍Java语言中的一些常见特性,来看一下字节码的应用,由于Java特性非常多,这里我们仅介绍一些经常遇到的特性。
Java字节码简介
Java字节码由单字节(byte)的指令组成,理论上最多支持256个操作码(opcode)。实际上Java只使用了200左右的操作码。
字节码指令可以大致分为5类:
- 栈操作指令,包括与局部变量交互的指令
- 程序流程控制指令
- 对象操作指令,包括方法调用指令
- 算术运算以及类型转换指令
- 特殊指令,比如同步(synchronization)指令,异常处理指令等
我们先来看一段简单的案例。下面是Java源代码
public class Demo {
private String s;
public Demo() {
this.s = "hello world";
}
public Demo(String s) {
this.s = s;
}
public void foo() {
System.out.println(s);
}
public static void main(String[] args) {
Demo demo = new Demo("shawn");
demo.foo();
}
}
通过javac编译后生成Demo.class文件,在通过javap -c -v Demo.class
指令查看操作码,结果如下:
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Classfile /E:/workspace/myself-projects/learning-notes/programming_languages/java-learning/deep-in-java/jvm/target/classes/com/shawn/study/deep/in/java/jvm/Demo.class
Last modified 2023-11-3; size 898 bytes
// MD5 CheckSum
MD5 checksum c9e1605d3b79bb7b1d0c9b51f4951ef5
Compiled from "Demo.java"
public class com.shawn.study.deep.in.java.jvm.Demo
// JDK version
minor version: 0
major version: 55
flags: ACC_PUBLIC, ACC_SUPER // 访问标识符
// 常量池
Constant pool:
#1 = Methodref #10.#30 // java/lang/Object."<init>":()V
#2 = String #31 // hello world
#3 = Fieldref #6.#32 // com/shawn/study/deep/in/java/jvm/Demo.s:Ljava/lang/String;
#4 = Fieldref #33.#34 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #35.#36 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #37 // com/shawn/study/deep/in/java/jvm/Demo
#7 = String #38 // shawn
#8 = Methodref #6.#39 // com/shawn/study/deep/in/java/jvm/Demo."<init>":(Ljava/lang/String;)V
#9 = Methodref #6.#40 // com/shawn/study/deep/in/java/jvm/Demo.foo:()V
#10 = Class #41 // java/lang/Object
#11 = Utf8 s
#12 = Utf8 Ljava/lang/String;
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/shawn/study/deep/in/java/jvm/Demo;
#20 = Utf8 (Ljava/lang/String;)V
#21 = Utf8 MethodParameters
#22 = Utf8 foo
#23 = Utf8 main
#24 = Utf8 ([Ljava/lang/String;)V
#25 = Utf8 args
#26 = Utf8 [Ljava/lang/String;
#27 = Utf8 demo
#28 = Utf8 SourceFile
#29 = Utf8 Demo.java
#30 = NameAndType #13:#14 // "<init>":()V
#31 = Utf8 hello world
#32 = NameAndType #11:#12 // s:Ljava/lang/String;
#33 = Class #42 // java/lang/System
#34 = NameAndType #43:#44 // out:Ljava/io/PrintStream;
#35 = Class #45 // java/io/PrintStream
#36 = NameAndType #46:#20 // println:(Ljava/lang/String;)V
#37 = Utf8 com/shawn/study/deep/in/java/jvm/Demo
#38 = Utf8 shawn
#39 = NameAndType #13:#20 // "<init>":(Ljava/lang/String;)V
#40 = NameAndType #22:#14 // foo:()V
#41 = Utf8 java/lang/Object
#42 = Utf8 java/lang/System
#43 = Utf8 out
#44 = Utf8 Ljava/io/PrintStream;
#45 = Utf8 java/io/PrintStream
#46 = Utf8 println
{
public com.shawn.study.deep.in.java.jvm.Demo();
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: ldc #2 // String hello world
7: putfield #3 // Field s:Ljava/lang/String;
10: return
LineNumberTable:
line 7: 0
line 8: 4
line 9: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/shawn/study/deep/in/java/jvm/Demo;
public com.shawn.study.deep.in.java.jvm.Demo(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #3 // Field s:Ljava/lang/String;
9: return
LineNumberTable:
line 11: 0
line 12: 4
line 13: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/shawn/study/deep/in/java/jvm/Demo;
0 10 1 s Ljava/lang/String;
MethodParameters:
Name Flags
s
public void foo();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: getfield #3 // Field s:Ljava/lang/String;
7: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
10: return
LineNumberTable:
line 16: 0
line 17: 10
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/shawn/study/deep/in/java/jvm/Demo;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: new #6 // class com/shawn/study/deep/in/java/jvm/Demo
3: dup
4: ldc #7 // String shawn
6: invokespecial #8 // Method "<init>":(Ljava/lang/String;)V
9: astore_1
10: aload_1
11: invokevirtual #9 // Method foo:()V
14: return
LineNumberTable:
line 20: 0
line 21: 10
line 22: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 args [Ljava/lang/String;
10 5 1 demo Lcom/shawn/study/deep/in/java/jvm/Demo;
MethodParameters:
Name Flags
args
}
SourceFile: "Demo.java"
javap -c -v能够看到更多的信息,比如常量池,本地变量表,MD5 Check等。
解读Java字节码
我们以其中一个代码片段为例,讲解一下其中的属性
public com.shawn.study.deep.in.java.jvm.Demo(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #3 // Field s:Ljava/lang/String;
9: return
LineNumberTable:
line 11: 0
line 12: 4
line 13: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/shawn/study/deep/in/java/jvm/Demo;
0 10 1 s Ljava/lang/String;
MethodParameters:
Name Flags
s
-
descriptor: 表示方法的描述。
- 其中小括号内是入参信息/形参信息;
- L表示对象;
- 后面的java/lang/String就是类名称;
- 小括号后面的 V 则表示这个方法的返回值是 void;
-
flags: 表示方法的访问标志,ACC_PUBLIC表示构造方法的访问标志为public。
-
Code: 是方法表,表示Java方法经过编译后的字节码指令,就是以字节码的形式表达Java方法的执行过程。
- stack:表示操作数栈的大小
- locals:表示局部变量表的大小
- args_size:表示方法形参的数量,但从源码来看这个构造方法的参数就1个啊,为什么args_size为2呢?实际上实例方法和构造方法除了方法形参的数量,还得加上对象实例的数量,也就是this。但静态方法是没有this引用的,所以计算args_size的时候无需+1,案例可以查看main方法的字节码指令
- LineNumberTable:表示java源代码(.java文件)的行号和字节码(.class文件)的行号之间的对应关系,主要是方便在异常发生的时候,在堆栈中显示出源代码出错的行号;以及在调试过程中,按照源代码的行号设置断点。
- LocalVariableTable:用来描述栈帧的局部变量表中变量与java源码中变量的对应关系。
-
MethodParameters:方法形参
Code表示方法表,记录了一堆字节码指令,如上所示:
- aload_0:用于加载局部方法表中的参数到操作数栈中。后面的0,表示局部变量表的Slot的位点。后面还有aload_1,就是用于加载局部变量s的。
- invokespecial:我们知道调用构造函数,会优先调用父类的构造函数,但这不是 JVM 自动执行的, 而是由程序指令控制,这个指令就是invokespecial。
- putfield:将值赋给实例字段
我们也能看到invokespecial #1
等指令后带着#,那这个又是代表什么意思呢?事实上,#表示对常量池的引用
Constant pool:
#1 = Methodref #10.#30 // java/lang/Object."<init>":()V
#2 = String #31 // hello world
#3 = Fieldref #6.#32 // com/shawn/study/deep/in/java/jvm/Demo.s:Ljava/lang/String;
#4 = Fieldref #33.#34 // java/lang/System.out:Ljava/io/PrintStream;
#5 = Methodref #35.#36 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #37 // com/shawn/study/deep/in/java/jvm/Demo
#7 = String #38 // shawn
- invokespecial #1: 引用的就是常量池里的
#1 = Methodref #10.#30
。 - putfield #3:引用的就是常量池里的
#3 = Fieldref #6.#32
。
常量池中的常量定义解读如下:以#1 = Methodref #10.#30
为例
- #1:常量编号, 该文件中其他地方可以引用。
- =:等号就是分隔符。
- Methodref:表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类指向的#10, 方法签名指向的 #30; 当然双斜线注释后面已经解析出来可读性比较好的说明了。
常量池就是一个常量的大字典,使用编号的方式把程序里用到的各类常量统一管理起来,这样在字节码操作里,只需要引用编号即可。
指令我们知道怎么解读,指令前面的数字是什么意思呢?0: aload_0
前面的0表示啥呢?
实际上就是.java中的方法源代码编译后让JVM真正执行的操作码(有一部分操作码会附带有操作数, 也会占用字节码数组中的空间)。为了帮助人们理解,反编译后看到的是十六进制操作码所对应的助记符,十六进制值操作码与助记符的对应关系,以及每一个操作码的用处可以查看Oracle官方文档进行了解,在需要用到时进行查阅即可,例如下面的 aload_n, invokespecial, putfield, return
操作指令
aload_n
会占用一个槽位,对应的字节码分别是
- aload_0 = 42 (0x2a)
- aload_1 = 43 (0x2b)
- aload_2 = 44 (0x2c)
- aload_3 = 45 (0x2d)
invokespecial会占用三个槽位:1个用于存放操作码指令自身,其余两个用于存放操作数。对应的格式如下:invokespecial indexbyte1 indexbyte2
对应的16进制指令是:invokespecial = 183 (0xb7)
putfield也会占用三个槽位,格式如下:putfield indexbyte1 indexbyte2
,对应的16进制指令是:putfield = 181 (0xb5)
return占用一个槽位,对应的16进制指令是:return = 177 (0xb1)
常见的指令集
操作数栈
想要深入了解字节码技术,我们需要先对字节码的执行模型有所了解。
每个线程都有一个独立线程栈(JVM stack),用于存储栈帧(Frame)。每一次方法调用,JVM都会自动创建一个栈帧。栈帧由操作数栈, 局部变量数组以及一个class引用组成。class引用指向当前方法在运行时常量池中对应的 class。
其中操作数栈,来存放计算的操作数以及返回结果。执行每一条指令之前,Java虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java 虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。
过程 | 符号 |
---|---|
变量到操作数栈 | iload, iload_, lload, lload_, fload, fload_, dload, dload_, aload, aload_ |
操作数栈到变量 | istore, istore_, lstore, lstore_, fstore, fstore_, dstore, dstor_, astore, astore_ |
常数到操作数栈 | bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_ml, iconst_, lconst_, fconst_, dconst_ |
把数据装载到操作数栈 | baload, caload, saload, iaload, laload, faload, daload, aaload |
从操作数栈存存储到数组 | bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore |
操作数栈管理 | pop, pop2, dup, dup2, dup_xl, dup2_xl, dup_x2, dup2_x2, swap |
算术指令集
Java 字节码中有许多指令可以执行算术运算。实际上,指令集中有很大一部分表示都是关于数学运算的。还有一部分用于类型转换
public class com.shawn.study.deep.in.java.jvm.AlgorithmDemo {
public com.shawn.study.deep.in.java.jvm.AlgorithmDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public int add(int, int);
Code:
0: iload_1
1: iload_2
2: iadd
3: ireturn
public double div(int, int);
Code:
0: iload_1
1: i2d
2: iload_2
3: i2d
4: ddiv
5: dreturn
public int multi(int, int);
Code:
0: iload_1
1: iload_2
2: imul
3: ireturn
public int sub(int, int);
Code:
0: iload_1
1: iload_2
2: isub
3: ireturn
public byte add(byte, byte);
Code:
0: iload_1
1: iload_2
2: iadd
3: i2b
4: ireturn
public short add(short, short);
Code:
0: iload_1
1: iload_2
2: iadd
3: i2s
4: ireturn
public java.lang.String concat(char, char);
Code:
0: iload_1
1: invokestatic #2 // Method java/lang/String.valueOf:(C)Ljava/lang/String;
4: iload_2
5: invokestatic #2 // Method java/lang/String.valueOf:(C)Ljava/lang/String;
8: invokevirtual #3 // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
11: areturn
public boolean merge(boolean, boolean);
Code:
0: iload_1
1: ifeq 12
4: iload_2
5: ifeq 12
8: iconst_1
9: goto 13
12: iconst_0
13: ireturn
public float add(float, float);
Code:
0: fload_1
1: fload_2
2: fadd
3: freturn
public double add(double, double);
Code:
0: dload_1
1: dload_3
2: dadd
3: dreturn
public long add(long, long);
Code:
0: lload_1
1: lload_3
2: ladd
3: lreturn
}
算术操作码和类型
对于所有数值类型(int, long, double, float),都有加,减,乘,除,取反的指令。对于byte和char, boolean,JVM是当做int来处理的
add(+) | sub(-) | multi(*) | divide(/) | remainder(%) | negate(-()) | |
---|---|---|---|---|---|---|
boolean | iadd | isub | imul | idiv | irem | ineg |
byte | iadd | isub | imul | idiv | irem | ineg |
short | iadd | isub | imul | idiv | irem | ineg |
int | iadd | isub | imul | idiv | irem | ineg |
char | iadd | isub | imul | idiv | irem | ineg |
long | ladd | lsub | lmul | ldiv | lrem | lneg |
float | fadd | fsub | fmul | fdiv | frem | fneg |
double | dadd | dsub | dmul | ddiv | drem | dneg |
类型转换操作码
byte | short | int | long | float | double | char | |
---|---|---|---|---|---|---|---|
int | i2b | i2s | - | i2l | i2f | i2d | i2c |
long | - | - | l2i | - | l2f | l2d | - |
float | - | - | f2i | f2l | - | f2d | - |
double | - | - | d2i | d2l | d2f | - | - |
按位运算符
public class BitAlgo {
public void testByte() {
byte a = 1;
byte b = 2;
int c = a << 2;
int d = a >> 2;
int e = b >>> 2;
int f = a | b;
int g = a & b;
int h = a ^ b;
}
public void testShort() {
short a = 1;
short b = 2;
int c = a << 2;
int d = a >> 2;
int e = b >>> 2;
int f = a | b;
int g = a & b;
int h = a ^ b;
}
public void testChar() {
char a = 1;
char b = 2;
int c = a << 2;
int d = a >> 2;
int e = b >>> 2;
int f = a | b;
int g = a & b;
int h = a ^ b;
}
public void testInt() {
int a = 1;
int b = 2;
int c = a << 2;
int d = a >> 2;
int e = b >>> 2;
int f = a | b;
int g = a & b;
int h = a ^ b;
}
public void testLong() {
long a = 1l;
long b = 2l;
long c = a << 2;
long d = a >> 2;
long e = b >>> 2;
long f = a | b;
long g = a & b;
long h = a ^ b;
}
}
编译后:
public class com.shawn.study.deep.in.java.jvm.BitAlgo {
public com.shawn.study.deep.in.java.jvm.BitAlgo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void testByte();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iconst_2
6: ishl
7: istore_3
8: iload_1
9: iconst_2
10: ishr
11: istore 4
13: iload_2
14: iconst_2
15: iushr
16: istore 5
18: iload_1
19: iload_2
20: ior
21: istore 6
23: iload_1
24: iload_2
25: iand
26: istore 7
28: iload_1
29: iload_2
30: ixor
31: istore 8
33: return
public void testShort();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iconst_2
6: ishl
7: istore_3
8: iload_1
9: iconst_2
10: ishr
11: istore 4
13: iload_2
14: iconst_2
15: iushr
16: istore 5
18: iload_1
19: iload_2
20: ior
21: istore 6
23: iload_1
24: iload_2
25: iand
26: istore 7
28: iload_1
29: iload_2
30: ixor
31: istore 8
33: return
public void testChar();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iconst_2
6: ishl
7: istore_3
8: iload_1
9: iconst_2
10: ishr
11: istore 4
13: iload_2
14: iconst_2
15: iushr
16: istore 5
18: iload_1
19: iload_2
20: ior
21: istore 6
23: iload_1
24: iload_2
25: iand
26: istore 7
28: iload_1
29: iload_2
30: ixor
31: istore 8
33: return
public void testInt();
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iconst_2
6: ishl
7: istore_3
8: iload_1
9: iconst_2
10: ishr
11: istore 4
13: iload_2
14: iconst_2
15: iushr
16: istore 5
18: iload_1
19: iload_2
20: ior
21: istore 6
23: iload_1
24: iload_2
25: iand
26: istore 7
28: iload_1
29: iload_2
30: ixor
31: istore 8
33: return
public void testLong();
Code:
0: lconst_1
1: lstore_1
2: ldc2_w #2 // long 2l
5: lstore_3
6: lload_1
7: iconst_2
8: lshl
9: lstore 5
11: lload_1
12: iconst_2
13: lshr
14: lstore 7
16: lload_3
17: iconst_2
18: lushr
19: lstore 9
21: lload_1
22: lload_3
23: lor
24: lstore 11
26: lload_1
27: lload_3
28: land
29: lstore 13
31: lload_1
32: lload_3
33: lxor
34: lstore 15
36: return
}
byte | short | int | long | float | double | char | |
---|---|---|---|---|---|---|---|
移位 | ishl/ishr/iushr | ishl/ishr/iushr | ishl/ishr/iushr | lshl/lshr/lushr | - | - | ishl/ishr/iushr |
按位或 | ior | ior | ior | lor | - | - | ior |
按位与 | iand | iand | iand | land | - | - | iand |
按位异或 | ixor | ixor | ixor | lxor | - | - | ixor |
流程指令集
源代码
public class ControlTests {
public void forLoop() {
int a = 49;
for (int i = 0; i < 100; i++) {
if (i == a) {
continue;
} else if (i == 56) {
break;
}
}
}
public void forEachLoop() {
int[] arr = new int[10];
Arrays.fill(arr, 1);
for (int i : arr) {
System.out.println(i);
}
}
public void whileLoop() {
int[] arr = new int[10];
Arrays.fill(arr, 1);
int i = 0;
while (i < arr.length) {
System.out.println(arr[i]);
i++;
}
}
public void doWhileLoop() {
int[] arr = new int[10];
Arrays.fill(arr, 1);
int i = 0;
do {
System.out.println(arr[i]);
i++;
} while (i < arr.length);
}
public void switchCase() {
int i = new Random().nextInt(2);
switch (i) {
case 0:
System.out.println(0);
break;
case 1:
System.out.println(1);
break;
default:
System.out.println("default");
break;
}
}
}
编译后(只截取了forLoop方法)
public void forLoop();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: bipush 49
2: istore_1
3: iconst_0
4: istore_2
5: iload_2
6: bipush 100
8: if_icmpge 34
11: iload_2
12: iload_1
13: if_icmpne 19
16: goto 28
19: iload_2
20: bipush 56
22: if_icmpne 28
25: goto 34
28: iinc 2, 1
31: goto 5
34: return
LineNumberTable:
line 9: 0
line 10: 3
line 11: 11
line 12: 16
line 13: 19
line 14: 25
line 10: 28
line 17: 34
LocalVariableTable:
Start Length Slot Name Signature
5 29 2 i I
0 35 0 this Lcom/shawn/study/deep/in/java/jvm/ControlTests;
3 32 1 a I
我们只分析forLoop方法。
根据LineNumberTable可知编号【3~28】用于循环控制
【8: if_icmpge 34 解读: if, integer, compare, great equal】, 如果一个数的值大于或等于另一个值,则程序执行流程跳转到pc=34的地方继续执行。
【goto】指的是跳转到哪里执行,值得一提的是,java还保留了goto关键字,但是java源代码中不会使用到。
【iinc】局部变量增加常量,是for循环中用于递增的循环计数器,根据【iinc 2, 1】可知,局部变量i加1后,然后在赋值给i。
- 条件分支:if、ifnull、ifnonnull、if_icmp等
- 复合分支:tableswitch、lookupswitch
- 无条件分支:goto、goto_w、jsr、jsr_w、ret
对象操作方法调用指令集
创建对象
我们都知道 new是 Java 编程语言中的一个关键字, 但其实在字节码中,也有一个指令叫做 new。 当我们创建类的实例时, 编译器会生成类似下面这样的操作码:
0: new #2 // class demo/jvm0104/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V
当你同时看到 new, dup 和 invokespecial 指令在一起时,那么一定是在创建类的实例对象!
为什么是三条指令而不是一条呢?这是因为:
- new 指令只是创建对象,但没有调用构造函数。
- invokespecial 指令用来调用某些特殊方法的, 当然这里调用的是构造函数。
- dup 指令用于复制栈顶的值。
由于构造函数调用不会返回值,所以如果没有 dup 指令, 在对象上调用方法并初始化之后,操作数栈就会是空的,在初始化之后就会出问题, 接下来的代码就无法对其进行处理。
这就是为什么要事先复制引用的原因,为的是在构造函数返回之后,可以将对象实例赋值给局部变量或某个字段。因此,接下来的那条指令一般是以下几种:
- astore {N} or astore_{N} – 赋值给局部变量,其中 {N} 是局部变量表中的位置。
- putfield – 将值赋给实例字段
- putstatic – 将值赋给静态字段
在调用构造函数的时候,其实还会执行另一个类似的方法 ,甚至在执行构造函数之前就执行了。
还有一个可能执行的方法是该类的静态初始化方法 , 但 并不能被直接调用,而是由这些指令触发的: new, getstatic, putstatic or invokestatic。
也就是说,如果创建某个类的新实例, 访问静态字段或者调用静态方法,就会触发该类的静态初始化方法【如果尚未初始化】。
实际上,还有一些情况会触发静态初始化, 详情请参考 JVM 规范: [docs.oracle.com/javase/spec…
方法调用
方法调用 | 含义 |
---|---|
invokevirtual | 用于调用公共,受保护和打包私有方法。 |
invokestatic | static方法 |
invokeinterface | 运行时搜索合适方法调用 |
invokespecial | 包括实例初始化方法、父类方法 |
invokedynamic | 运行时动态解析出调用点限定符所引用方法 |
方法返回
方法返回 | 含义 |
---|---|
ireturn | 当前方法返回int |
lreturn | 当前方法返回long |
freturn | 当前方法返回float |
dreturn | 当前方法返回double |
areturn | 当前方法返回ref |
异常处理指令集
public void execute() {
int i = 0;
try {
i = 100 / 0;
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
i = 1;
}
}
编译后
public void execute();
Code:
stack=3, locals=4, args_size=1
0: iconst_0
1: istore_1
2: bipush 100
4: iconst_0
5: idiv
6: istore_1
7: iconst_1
8: istore_1
9: goto 27
12: astore_2
13: new #3 // class java/lang/RuntimeException
16: dup
17: aload_2
18: invokespecial #4 // Method java/lang/RuntimeException."<init>":(Ljava/lang/Throwable;)V
21: athrow
22: astore_3
23: iconst_1
24: istore_1
25: aload_3
26: athrow
27: return
Exception table:
from to target type
2 7 12 Class java/lang/Exception
2 7 22 any
12 23 22 any
LineNumberTable:
line 6: 0
line 8: 2
line 12: 7
line 13: 9
line 9: 12
line 10: 13
line 12: 22
line 13: 25
line 14: 27
LocalVariableTable:
Start Length Slot Name Signature
13 9 2 e Ljava/lang/Exception;
0 28 0 this Lcom/shawn/study/deep/in/java/jvm/ExceptionDemo;
2 26 1 i I
Java程序显式抛出异常: athrow指令。在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现,而是采用带有一个叫 Exception table 的异常表来完成的:
- from 指定字节码索引的开始位置
- to 指定字节码索引的结束位置
- target 异常处理的起始位置
- type 异常类型
也就是说,只要在 from 和 to 之间发生了异常,就会跳转到 target 所指定的位置。当type为any时,表示无论如何都需要跳转到target位置继续执行。
语法糖
装箱拆箱
Integer a = 1000;
int b = a * 10;
return b;
编译后
0: sipush 1000
3: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
6: astore_1
7: aload_1
8: invokevirtual #3 // Method java/lang/Integer.intValue:()I
11: bipush 10
13: imul
14: istore_2
15: iload_2
16: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: areturn
通过观察字节码,我们发现赋值操作使用的是 Integer.valueOf 方法,在进行乘法运算的时候,调用了 Integer.intValue 方法来获取基本类型的值。在方法返回的时候,再次使用了 Integer.valueOf 方法对结果进行了包装。
这就是 Java 中的自动装箱拆箱的底层实现。
注解
public @interface MyAnnotation {
}
@MyAnnotation
public class AnnotationDemo {
@MyAnnotation
public void test(@MyAnnotation int a){
}
}
编译后
{
public AnnotationDemo();
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 2: 0
public void test(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 6: 0
RuntimeInvisibleAnnotations:
0: #11()
RuntimeInvisibleParameterAnnotations:
0:
0: #11()
}
SourceFile: "AnnotationDemo.java"
RuntimeInvisibleAnnotations:
0: #11()
可以看到,无论是类的注解,还是方法注解,都是由一个叫做 RuntimeInvisibleAnnotations 的结构来存储的,而参数的存储,是由 RuntimeInvisibleParameterAnotations 来保证的。