• 字节码学习之常见java语句的底层原理


    前言

    上一章我们聊了《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");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以使用javap命令来反编译字节码:

    javap -c IfStatement.class
    
    • 1

    输出结果如下:

    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                      // 方法返回
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    字节码的解析

    • 在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);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以使用javap命令来反编译字节码:

    javap -c ForLoop.class
    
    • 1

    这是一个包含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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    字节码的解析

    可以看到,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++;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    可以使用javap命令来反编译字节码:

    javap -c WhileLoop.class
    
    • 1

    输出结果如下:

    public class WhileLoop {
      public WhileLoop();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."":()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."":()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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    可以看到,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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    可以使用javap命令来反编译字节码:

    javap -c SwitchStatement.class
    
    • 1

    输出结果如下:

    public class SwitchStatement {
      public SwitchStatement();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."":()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 ]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    可以看到,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!");
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以使用javap命令来反编译字节码:

    javap -c TryCatchStatement.class
    
    • 1

    输出结果如下:

    public class TryCatchStatement {
      public TryCatchStatement();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."":()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 ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    可以看到,try-catch语句的底层实现是通过异常表来实现的。在本例中,当数组下标越界时,会执行第12行的输出语句;否则,会跳转到第20行,继续执行。

    6. i++ 和++i的字节码

    i++++i 在语义上有些许不同,在字节码层面也有所体现。下面是它们的字节码层面的解释:

    假设 i 是局部变量表的索引为1的变量。

    i++ 的伪字节码:

    iload_1                 // 从局部变量表中加载变量 i 到操作数栈顶
    iinc 1 by 1             // 将局部变量表中的变量 i 增加1
    
    • 1
    • 2

    ++i 的伪字节码:

    iinc 1 by 1             // 将局部变量表中的变量 i 增加1
    iload_1                 // 从局部变量表中加载变量 i 到操作数栈顶
    
    • 1
    • 2

    可以看到,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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    对应的字节码指令

    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 指令之后的代码
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    这段字节码中使用了 jsrret 指令,这两个指令主要用于实现 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. 参考文档

    1. 张亚 《深入理解JVM字节码》
    2. https://www.jonesjalapat.com/2021/09/11/internal-working-of-java-virtual-machine/
  • 相关阅读:
    《算法导论》第16章-贪心算法 16.1-活动选择问题(含C++代码)
    fwrite 多出一个0x0D字节的问题
    Go 事,如何成为一个Gopher ,并在7天找到 Go 语言相关工作,第1篇
    MyBatis的缓存机制(包含一级缓存和二级缓存等所有配置)
    博菱电器创业板过会:收入依赖单一客户,“创二代”袁琪本科肄业
    微信小程序中封装请求,使用Async await方法,将异步请求变为同步请求方法
    DS200DCFBG1BLC IS220PAICH1A 构建人工智能能力背后的紧迫性
    服务器硬盘HDD与SSD的优缺点是什么
    P1040 [NOIP2003 提高组] 加分二叉树
    jsp基站管理系统servlet开发sqlserver数据库MVC结构java编程计算机网页项目
  • 原文地址:https://blog.csdn.net/wangshuai6707/article/details/133829780