文章目录
- 前言
- 1. if语句
- 字节码的解析
- 2. for循环
- 字节码的解析
- 3. while循环
- 4. switch语句
- 5. try-catch语句
- 6. i++ 和++i的字节码
- 7. try-catch-finally
- 8. 参考文档
前言
上一章我们聊了《JVM字节码指令详解》 。本章我们学以致用,聊一下我们常见的一些java语句的特性底层是如何实现。
1. if语句
if语句是我们最常用的判断语句之一,它的底层实现原理是什么呢?可以通过反编译字节码来分析一下。
假设我们有以下的java代码:
public class IfStatement {
public static void main(String[] args) {
int a = 10;
if (a > 0) {
System.out.println("a is positive");
} else {
System.out.println("a is negative or zero");
}
}
}
可以使用javap命令来反编译字节码:
javap -c IfStatement.class
输出结果如下:
public class IfStatement {
// 构造方法
public IfStatement();
Code:
0: aload_0 // 将局部变量表中的第0个元素(通常是this引用)入栈
1: invokespecial #1 // 调用父类Object的构造方法
4: return // 方法返回
// main方法
public static void main(java.lang.String[]);
Code:
0: bipush 10 // 将10压入栈顶
2: istore_1 // 将栈顶元素(10)存入局部变量表的第1个位置
3: iload_1 // 将局部变量表的第1个位置的元素(10)入栈
4: ifle 17 // 判断栈顶元素(10)是否小于等于0,如果是则跳转到指令17
7: getstatic #2 // 获取System类的out字段,类型是PrintStream
10: ldc #3 // 将字符串"a is positive"压入栈顶
12: invokevirtual #4 // 调用PrintStream的println方法输出字符串
15: goto 23 // 无条件跳转到指令23
18: getstatic #2 // 获取System类的out字段,类型是PrintStream
21: invokevirtual #5 // 调用PrintStream的println方法输出局部变量表第1个位置的元素(10)
24: return // 方法返回
}
字节码的解析
- 在main方法中,首先将10压入栈顶,然后将其存入局部变量表的第1个位置。然后将局部变量表的第1个位置的元素(10)入栈,判断其是否小于等于0,如果是则跳转到指令17,否则执行下一条指令。在指令7-15中,它获取System的out字段,将字符串"a is positive"压入栈顶,然后调用println方法输出这个字符串,最后无条件跳转到指令23。在指令18-21中,它获取System的out字段,然后调用println方法输出局部变量表第1个位置的元素(10)。最后,main方法返回。
2. for循环
for循环是我们常用的循环语句之一,它的底层实现原理是什么呢?我们还是可以通过反编译字节码来分析一下。
假设我们有以下的java代码:
public class ForLoop {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
System.out.println("i = " + i);
}
}
}
可以使用javap命令来反编译字节码:
javap -c ForLoop.class
这是一个包含for循环的Java类ForLoop的字节码输出,下面是中文注释:
public class ForLoop {
// 构造方法
public ForLoop();
Code:
0: aload_0 // 将局部变量表中的第0个元素(通常是this引用)入栈
1: invokespecial #1 // 调用父类Object的构造方法
4: return // 方法返回
// main方法
public static void main(java.lang.String[]);
Code:
0: iconst_0 // 将0压入栈顶
1: istore_1 // 将栈顶元素(0)存入局部变量表的第1个位置
2: iload_1 // 将局部变量表的第1个位置的元素(0)入栈
3: bipush 10 // 将10压入栈顶
5: if_icmpge 19 // 如果局部变量表的第1个位置的元素(0)大于等于栈顶元素(10),则跳转到指令19
8: getstatic #2 // 获取System类的out字段,类型是PrintStream
11: new #3 // 创建一个StringBuilder类的对象
14: dup // 复制栈顶元素,此时栈顶有两个相同的StringBuilder对象引用
15: invokespecial #4 // 调用StringBuilder类的构造函数初始化对象
18: ldc #5 // 将字符串"i ="压入栈顶
20: invokevirtual #6 // 调用StringBuilder的append方法将字符串添加到StringBuilder
23: iload_1 // 将局部变量表的第1个位置的元素(0)入栈
24: invokevirtual #7 // 调用StringBuilder的append方法将数字添加到StringBuilder
27: invokevirtual #8 // 调用StringBuilder的toString方法将StringBuilder转化为字符串
30: invokevirtual #9 // 调用PrintStream的println方法输出字符串
33: iinc 1, 1 // 将局部变量表的第1个位置的元素(0)增加1
36: goto 2 // 无条件跳转到指令2,形成循环
39: return // 方法返回
LineNumberTable:
line 3: 0
line 4: 8
line 3: 33
line 6: 39
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 2
locals = [ int, int ]
stack = []
frame_type = 250 /* chop */
offset_delta = 36
}
字节码的解析
可以看到,for循环的底层实现是通过if_icmpge指令来实现的。在本例中,当i小于10时,会执行第8行的输出语句;否则,会跳转到第39行,结束循环。
- 在main方法中,首先将0压入栈顶,然后将其存入局部变量表的第1个位置。接下来是一个循环,循环条件是局部变量表的第1个位置的元素小于10。在循环体中,它首先获取System的out字段,然后创建一个StringBuilder对象并初始化,然后将字符串"i ="和局部变量表的第1个位置的元素添加到StringBuilder,然后将StringBuilder转化为字符串,然后调用println方法输出字符串。在循环体结束时,它将局部变量表的第1个位置的元素加1,然后无条件跳转到指令2,形成循环。当循环结束时,main方法返回。
3. while循环
while循环是我们常用的循环语句之一,它的底层实现原理是什么呢?我们还是可以通过反编译字节码来分析一下。
假设我们有以下的java代码:
public class WhileLoop {
public static void main(String[] args) {
int i = 0;
while (i < 10) {
System.out.println("i = " + i);
i++;
}
}
}
可以使用javap命令来反编译字节码:
javap -c WhileLoop.class
输出结果如下:
public class WhileLoop {
public WhileLoop();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 19
8: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
11: new #3 // class java/lang/StringBuilder
14: dup
15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
18: ldc #5 // String i =
20: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
23: iload_1
24: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
27: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
30: invokevirtual #9 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: iinc 1, 1
36: goto 2
39: return
LineNumberTable:
line 3: 0
line 4: 2
line 5: 8
line 6: 33
line 5: 36
line 8: 39
StackMapTable: number_of_entries = 2
frame_type = 252 /* append */
offset_delta = 2
locals = [ int, int ]
stack = []
frame_type = 250 /* chop */
offset_delta = 36
}
可以看到,while循环的底层实现也是通过if_icmpge指令来实现的。在本例中,当i小于10时,会执行第8行的输出语句;否则,会跳转到第39行,结束循环。
4. switch语句
switch语句是我们常用的分支语句之一,它的底层实现原理是什么呢?我们还是可以通过反编译字节码来分析一下。
假设我们有以下的java代码:
public class SwitchStatement {
public static void main(String[] args) {
int i = 2;
switch (i) {
case 1:
System.out.println("i is 1");
break;
case 2:
System.out.println("i is 2");
break;
case 3:
System.out.println("i is 3");
break;
default:
System.out.println("i is neither 1, 2 nor 3");
break;
}
}
}
可以使用javap命令来反编译字节码:
javap -c SwitchStatement.class
输出结果如下:
public class SwitchStatement {
public SwitchStatement();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_2
1: istore_1
2: iload_1
3: tableswitch { // 1 to 3
1: 28
2: 40
3: 52
default: 64
}
28: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
31: ldc #3 // String i is 1
33: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
36: goto 71
40: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
43: ldc #5 // String i is 2
45: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
48: goto 71
52: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
55: ldc #6 // String i is 3
57: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
60: goto 71
64: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
67: invokevirtual #7 // Method java/io/PrintStream.println:()V
70: return
71: return
LineNumberTable:
line 3: 0
line 4: 2
line 5: 28
line 6: 40
line 7: 52
line 8: 64
line 9: 70
line 7: 71
StackMapTable: number_of_entries = 5
frame_type = 252 /* append */
offset_delta = 28
locals = [ int ]
frame_type = 252 /* append */
offset_delta = 11
locals = [ int ]
frame_type = 252 /* append */
offset_delta = 12
locals = [ int ]
frame_type = 252 /* append */
offset_delta = 12
locals = [ int ]
frame_type = 252 /* append */
offset_delta = 3
locals = [ int ]
可以看到,switch语句的底层实现是通过tableswitch指令来实现的。在本例中,当i等于1时,会执行第28行的输出语句;当i等于2时,会执行第40行的输出语句;当i等于3时,会执行第52行的输出语句;否则,会执行第64行的输出语句。
5. try-catch语句
try-catch语句是我们常用的异常处理语句之一,它的底层实现原理是什么呢?我们还是可以通过反编译字节码来分析一下。
假设我们有以下的java代码:
public class TryCatchStatement {
public static void main(String[] args) {
try {
int[] arr = new int[3];
arr[4] = 5;
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("Array index out of bounds!");
}
}
}
可以使用javap命令来反编译字节码:
javap -c TryCatchStatement.class
输出结果如下:
public class TryCatchStatement {
public TryCatchStatement();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: newarray int
3: astore_1
4: aload_1
5: iconst_4
6: iconst_5
7: iastore
8: goto 19
11: astore_1
12: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #3 // String Array index out of bounds!
17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
20: return
Exception table:
from to target type
0 8 11 Class java/lang/ArrayIndexOutOfBoundsException
LineNumberTable:
line 3: 0
line 4: 4
line 5: 11
line 6: 12
line 7: 20
StackMapTable: number_of_entries = 2
frame_type = 34 /* same */
frame_type = 1 /* same_locals_1_stack_item */
stack = [ class java/lang/ArrayIndexOutOfBoundsException ]
}
可以看到,try-catch语句的底层实现是通过异常表来实现的。在本例中,当数组下标越界时,会执行第12行的输出语句;否则,会跳转到第20行,继续执行。
6. i++ 和++i的字节码
i++
和 ++i
在语义上有些许不同,在字节码层面也有所体现。下面是它们的字节码层面的解释:
假设 i
是局部变量表的索引为1的变量。
i++
的伪字节码:
iload_1 // 从局部变量表中加载变量 i 到操作数栈顶
iinc 1 by 1 // 将局部变量表中的变量 i 增加1
++i
的伪字节码:
iinc 1 by 1 // 将局部变量表中的变量 i 增加1
iload_1 // 从局部变量表中加载变量 i 到操作数栈顶
可以看到,i++
与 ++i
的主要区别在于加载和增加操作的顺序不同。i++
是先将 i
加载到操作数栈顶,然后再增加 i
的值;而 ++i
是先增加 i
的值,然后再将 i
加载到操作数栈顶。这就解释了 i++
和 ++i
在语义上的不同:i++
是先取值后加1,++i
是先加1后取值。
7. try-catch-finally
在 Java 字节码中,try-catch-finally 结构主要通过异常表(Exception Table)来实现。Java 字节码并没有专门的指令来表示 try、catch 或者 finally 块。相反,它通过在异常表中记录 try 块的开始和结束位置、catch 块的开始位置和要捕获的异常类型,以实现异常处理的流程。
下面是一个简单的 try-catch-finally 代码例子:
void test() {
try {
System.out.println("try block");
throw new Exception();
} catch (Exception e) {
System.out.println("catch block");
} finally {
System.out.println("finally block");
}
}
对应的字节码指令
0: getstatic #2 // 获取 java/lang/System 类的 out 字段,是 PrintStream 类型
3: ldc #3 // 将常量池中的 "try block" 字符串压入栈顶
5: invokevirtual #4 // 调用 PrintStream 类的 println 方法输出字符串
8: new #5 // 创建一个 java/lang/Exception 类的对象
11: dup // 复制栈顶的元素,此时栈顶有两个相同的异常对象引用
12: invokespecial #6 // 调用 Exception 类的构造函数初始化对象
15: athrow // 抛出栈顶的异常对象
16: astore_1 // 捕获异常并存入局部变量表的第1个位置
17: getstatic #2 // 获取 java/lang/System 类的 out 字段,是 PrintStream 类型
20: ldc #7 // 将常量池中的 "catch block" 字符串压入栈顶
22: invokevirtual #4 // 调用 PrintStream 类的 println 方法输出字符串
25: jsr 26 // 无条件跳转到指令26(finally块的起始位置)
28: goto 34 // 执行完finally块后,跳过 catch 块剩下的代码,进入下一个处理流程
31: astore_2 // 捕获从finally块抛出的异常并存入局部变量表的第2个位置
32: jsr 26 // 无条件跳转到指令26(finally块的起始位置)
35: aload_2 // 从局部变量表的第2个位置加载异常对象至栈顶
36: athrow // 再次抛出该异常对象
37: astore_3 // 捕获异常并存入局部变量表的第3个位置
38: getstatic #2 // 获取 java/lang/System 类的 out 字段,是 PrintStream 类型
41: ldc #8 // 将常量池中的 "finally block" 字符串压入栈顶
43: invokevirtual #4 // 调用 PrintStream 类的 println 方法输出字符串
46: ret 3 // 返回到 astore_3 指令之后的代码
这段字节码中使用了 jsr
和 ret
指令,这两个指令主要用于实现 finally
块的逻辑。jsr
指令会跳转到 finally
块的代码,然后 ret
指令用于返回到 finally
块之前的代码继续执行。
字节码的解释
-
行0-15:这部分对应 try 块的内容。在这个例子中,它首先通过 getstatic 指令获取 System.out 对象,然后通过 ldc 指令加载常量 “try block”,最后调用 println 方法输出这个字符串。然后,它创建一个 Exception 对象并抛出。
-
行16-25:这部分对应 catch 块的内容。当 try 块抛出异常时,执行流程会跳转到这部分。在这个例子中,它首先通过 astore 指令将异常对象存储到局部变量表,然后类似于 try 块的处理,输出 “catch block” 字符串。
-
行37-46:这部分对应 finally 块的内容。无论 try 块是否抛出异常,这部分代码总是会被执行。在这个例子中,它输出 “finally block” 字符串。
-
行25-32和行35-36:这部分是对异常处理的一些额外控制。jsr 和 ret 指令用于实现无条件的跳转,确保 finally 块总是会被执行。
8. 参考文档
- 张亚 《深入理解JVM字节码》
- https://www.jonesjalapat.com/2021/09/11/internal-working-of-java-virtual-machine/