Java字节码学习笔记(二):Java字节码怎么看?

1、字节码查看方式

    .class 文件本身是二进制字节码,直接看的话太晦涩难懂,我们这边看的时候借助一些反汇编工具来查看。

1.1、javap

    javap 可以反编译字节码文件。通过 javap -help 命令可以了解 javap 的基本用法。

D:\workspace\DemoTest\out\production\DemoTest\com\leo\test>javap -help
用法: javap <options> <classes>
其中, 可能的选项包括:
  -help  --help  -?        输出此用法消息
  -version                 版本信息
  -v  -verbose             输出附加信息
  -l                       输出行号和本地变量表
  -public                  仅显示公共类和成员
  -protected               显示受保护的/公共类和成员
  -package                 显示程序包/受保护的/公共类
                           和成员 (默认)
  -p  -private             显示所有类和成员
  -c                       对代码进行反汇编
  -s                       输出内部类型签名
  -sysinfo                 显示正在处理的类的
                           系统信息 (路径, 大小, 日期, MD5 散列)
  -constants               显示最终常量
  -classpath <path>        指定查找用户类文件的位置
  -cp <path>               指定查找用户类文件的位置
  -bootclasspath <path>    覆盖引导类文件的位置

1.2、jclasslib

    jclasslib Bytecode Viewer 是 IDEA 开发工具中的一个插件,可以方便查看每个 java 类编译后的字节码文件。具体安装方法很多,不再赘述。
jclasslib 插件

2、字节码解析

2.1、编译前代码

package com.leo.test;

public class ByteCodeTest {
    
    
    public static int num = 1;

    public static int add(byte a, short b, int c, String d, float e, double f) {
    
    
        int d_int = Integer.parseInt(d);
        return (int) (a + b + c + d_int + e + f);
    }

    public static void main(String[] args) {
    
    
        byte a = (byte) 0xFF; // -1
        short b = 2;
        int c = 3;
        String d = "4";
        float e = 5.0f;
        double f = 6.0d;
        boolean bool = false;
        int g = add(a, b, c, d, e, f);
        System.out.println(num); // 1
        System.out.println(g); // 19
    }
}

2.2、编译后

使用 javap 命令查看字节码:

javap -c -verbose ByteCodeTest.class

使用 javap 反编译后的文件内容如下:

Classfile /D:/workspace/DemoTest/out/production/DemoTest/com/leo/test/ByteCodeTest.class
  Last modified 2022-6-28; size 1136 bytes
  MD5 checksum 6550d8d70ee59d88e1ec1614d1b6f1e1
  Compiled from "ByteCodeTest.java"
public class com.leo.test.ByteCodeTest
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#46        // java/lang/Object."<init>":()V
   #2 = Methodref          #47.#48        // java/lang/Integer.parseInt:(Ljava/lang/String;)I
   #3 = String             #49            // 4
   #4 = Float              5.0f
   #5 = Double             6.0d
   #7 = Methodref          #11.#50        // com/leo/test/ByteCodeTest.add:(BSILjava/lang/String;FD)I
   #8 = Fieldref           #51.#52        // java/lang/System.out:Ljava/io/PrintStream;
   #9 = Fieldref           #11.#53        // com/leo/test/ByteCodeTest.num:I
  #10 = Methodref          #54.#55        // java/io/PrintStream.println:(I)V
  #11 = Class              #56            // com/leo/test/ByteCodeTest
  #12 = Class              #57            // java/lang/Object
  #13 = Utf8               num
  #14 = Utf8               I
  #15 = Utf8               <init>
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               Lcom/leo/test/ByteCodeTest;
  #22 = Utf8               add
  #23 = Utf8               (BSILjava/lang/String;FD)I
  #24 = Utf8               a
  #25 = Utf8               B
  #26 = Utf8               b
  #27 = Utf8               S
  #28 = Utf8               c
  #29 = Utf8               d
  #30 = Utf8               Ljava/lang/String;
  #31 = Utf8               e
  #32 = Utf8               F
  #33 = Utf8               f
  #34 = Utf8               D
  #35 = Utf8               d_int
  #36 = Utf8               main
  #37 = Utf8               ([Ljava/lang/String;)V
  #38 = Utf8               args
  #39 = Utf8               [Ljava/lang/String;
  #40 = Utf8               bool
  #41 = Utf8               Z
  #42 = Utf8               g
  #43 = Utf8               <clinit>
  #44 = Utf8               SourceFile
  #45 = Utf8               ByteCodeTest.java
  #46 = NameAndType        #15:#16        // "<init>":()V
  #47 = Class              #58            // java/lang/Integer
  #48 = NameAndType        #59:#60        // parseInt:(Ljava/lang/String;)I
  #49 = Utf8               4
  #50 = NameAndType        #22:#23        // add:(BSILjava/lang/String;FD)I
  #51 = Class              #61            // java/lang/System
  #52 = NameAndType        #62:#63        // out:Ljava/io/PrintStream;
  #53 = NameAndType        #13:#14        // num:I
  #54 = Class              #64            // java/io/PrintStream
  #55 = NameAndType        #65:#66        // println:(I)V
  #56 = Utf8               com/leo/test/ByteCodeTest
  #57 = Utf8               java/lang/Object
  #58 = Utf8               java/lang/Integer
  #59 = Utf8               parseInt
  #60 = Utf8               (Ljava/lang/String;)I
  #61 = Utf8               java/lang/System
  #62 = Utf8               out
  #63 = Utf8               Ljava/io/PrintStream;
  #64 = Utf8               java/io/PrintStream
  #65 = Utf8               println
  #66 = Utf8               (I)V
{
    
    
  public static int num;
    descriptor: I
    flags: ACC_PUBLIC, ACC_STATIC

  public com.leo.test.ByteCodeTest();
    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 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/leo/test/ByteCodeTest;

  public static int add(byte, short, int, java.lang.String, float, double);
    descriptor: (BSILjava/lang/String;FD)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=8, args_size=6
         0: aload_3
         1: invokestatic  #2                  // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I
         4: istore        7
         6: iload_0
         7: iload_1
         8: iadd
         9: iload_2
        10: iadd
        11: iload         7
        13: iadd
        14: i2f
        15: fload         4
        17: fadd
        18: f2d
        19: dload         5
        21: dadd
        22: d2i
        23: ireturn
      LineNumberTable:
        line 7: 0
        line 8: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      24     0     a   B
            0      24     1     b   S
            0      24     2     c   I
            0      24     3     d   Ljava/lang/String;
            0      24     4     e   F
            0      24     5     f   D
            6      18     7 d_int   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=7, locals=10, args_size=1
         0: iconst_m1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iconst_3
         5: istore_3
         6: ldc           #3                  // String 4
         8: astore        4
        10: ldc           #4                  // float 5.0f
        12: fstore        5
        14: ldc2_w        #5                  // double 6.0d
        17: dstore        6
        19: iconst_0
        20: istore        8
        22: iload_1
        23: iload_2
        24: iload_3
        25: aload         4
        27: fload         5
        29: dload         6
        31: invokestatic  #7                  // Method add:(BSILjava/lang/String;FD)I
        34: istore        9
        36: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: getstatic     #9                  // Field num:I
        42: invokevirtual #10                 // Method java/io/PrintStream.println:(I)V
        45: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        48: iload         9
        50: invokevirtual #10                 // Method java/io/PrintStream.println:(I)V
        53: return
      LineNumberTable:
        line 12: 0
        line 13: 2
        line 14: 4
        line 15: 6
        line 16: 10
        line 17: 14
        line 18: 19
        line 19: 22
        line 20: 36
        line 21: 45
        line 22: 53
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      54     0  args   [Ljava/lang/String;
            2      52     1     a   B
            4      50     2     b   S
            6      48     3     c   I
           10      44     4     d   Ljava/lang/String;
           14      40     5     e   F
           19      35     6     f   D
           22      32     8  bool   Z
           36      18     9     g   I

  static {
    
    };
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: iconst_1
         1: putstatic     #9                  // Field num:I
         4: return
      LineNumberTable:
        line 4: 0
}
SourceFile: "ByteCodeTest.java"

2.3、字节码结构

在这里插入图片描述

引用 Oracle 官方给的字节码结构如下:

ClassFile {
    
    
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

解释一下

类型 名称 解释 长度 数量
u4 magic 魔数 4个字节 1
u2 minor_version 次版本号 2个字节 1
u2 major_version 主版本号 2个字节 1
u2 constant_pool_count 常量池大小 2个字节 1
cp_info constant_pool[] 常量池 n个字节 costant_pool_count - 1
u2 access_flags 类的访问控制权限 2个字节 1
u2 this_class 类索引 2个字节 1
u2 super_class 父类索引 2个字节 1
u2 interfaces_count 接口索引数量 2个字节 1
u2 interfaces[] 实现的接口内容 2个字节 interfaces_count
u2 fields_count 成员属性数量 2个字节 1
field_info fields[] 成员属性值 n个字节 fields_count
u2 methods_count 方法数量 2个字节 1
method_info methods[] 方法表 n个字节 methods_count
u2 attributes_count 类属性数量 2个字节 1
attribute_info attributes[] 类属性值 n个字节 attributes_count
  • 无符号数属于最基本的数据类型。它以 u1、u2、u4、u8 分别代表 1 个字节、2 个字节、4 个字节、8 个字节的无符号数。无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成的字符串值。例如下表中第一行中的 u4 表示 Class 文件前 4 个字节表示该文件的魔数,第二行的 u2 表示该 Class 文件第 5-6 个字节表示该 JDK 的次版本号。

  • 表是由多个无符号数或者其他表作为数据项构成的复合数据类型。所有表都习惯性地以_info结尾。表用于描述有层次关系的复合结构的数据,例如下表第 5 行表示其实一个类型为 cp_info 的表(常量池),这里面存储了该类的所有常量。

借用一张 Class文件字节码结构组织示意图
组织示意图

2.4、简化理解字节码结构

2.4.1、Class文件结构

    典型的class文件分为:MagicNumber,Version,Constant_pool,Access_flag,This_class,Super_class,Interfaces,Fields,Methods 和Attributes这十个部分。
    但是为了方便理解,借用简化后的模型:Class摘要常量池方法栈帧

2.4.2、Class 摘要

    摘要主要就是记录class的一些基本信息,包括类的大小、修改时间、校验和、JDK版本信息、类的访问范围等。

D:\workspace\DemoTest\out\production\DemoTest\com\leo\test>javap -v ByteCodeTest.class // 反编译字节码文件 ByteCodeTest.class
Classfile /D:/workspace/DemoTest/out/production/DemoTest/com/leo/test/ByteCodeTest.class // 文件来源,可以看到文件的路径
  Last modified 2022-6-28; size 1136 bytes // 类最后修改时间、类大小
  MD5 checksum 6550d8d70ee59d88e1ec1614d1b6f1e1 // 校验和
  Compiled from "ByteCodeTest.java"	// 编译的文件
public class com.leo.test.ByteCodeTest // 全类名
  minor version: 0 // jdk 子版本号
  major version: 52 // jdk 主版本号,52 代表的就是 jdk1.8
  flags: ACC_PUBLIC, ACC_SUPER // 说明这个类是 public 类,初始化方法使用父类的

修饰符含义

2.4.3、常量池

  • 字面量:包括文本字符串,final修饰的成员变量,还有数据的值等,对于基本数值int类型的常量,常量池值保存了引用和字面名称,没有保存数据的值
   #3 = String             #49            // 4,引用的是 #49 行的值
   #4 = Float              5.0f			  // float 类型 5.0
   #5 = Double             6.0d           // double 类型 6.0
   #28 = Utf8               c             // int 类型只保存了名称,没有存实际值
   ......中间省略了
   #49 = Utf8               4
  • 符号引用:类、接口的名称和描述
   #7 = Methodref          #11.#50        // com/leo/test/ByteCodeTest.add:(BSILjava/lang/String;FD)I
   #8 = Fieldref           #51.#52        // java/lang/System.out:Ljava/io/PrintStream;
   #9 = Fieldref           #11.#53        // com/leo/test/ByteCodeTest.num:I
  #10 = Methodref          #54.#55        // java/io/PrintStream.println:(I)V
  #11 = Class              #56            // com/leo/test/ByteCodeTest
  #12 = Class              #57            // java/lang/Object
  #57 = Utf8               java/lang/Object
   ......中间省略了
  #50 = NameAndType        #22:#23        // add:(BSILjava/lang/String;FD)I
  #51 = Class              #61            // java/lang/System
  #52 = NameAndType        #62:#63        // out:Ljava/io/PrintStream;
  #53 = NameAndType        #13:#14        // num:I
  #54 = Class              #64            // java/io/PrintStream
  #55 = NameAndType        #65:#66        // println:(I)V

2.4.4、方法栈帧

代码样例里面给的两个方法 add 和 main 方法:

  public static int add(byte, short, int, java.lang.String, float, double);
    descriptor: (BSILjava/lang/String;FD)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=8, args_size=6
         0: aload_3
         1: invokestatic  #2                  // Method java/lang/Integer.parseInt:(Ljava/lang/String;)I
         4: istore        7
         6: iload_0
         7: iload_1
         8: iadd
         9: iload_2
        10: iadd
        11: iload         7
        13: iadd
        14: i2f
        15: fload         4
        17: fadd
        18: f2d
        19: dload         5
        21: dadd
        22: d2i
        23: ireturn
      LineNumberTable:
        line 7: 0
        line 8: 6
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      24     0     a   B
            0      24     1     b   S
            0      24     2     c   I
            0      24     3     d   Ljava/lang/String;
            0      24     4     e   F
            0      24     5     f   D
            6      18     7 d_int   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=7, locals=10, args_size=1
         0: iconst_m1
         1: istore_1
         2: iconst_2
         3: istore_2
         4: iconst_3
         5: istore_3
         6: ldc           #3                  // String 4
         8: astore        4
        10: ldc           #4                  // float 5.0f
        12: fstore        5
        14: ldc2_w        #5                  // double 6.0d
        17: dstore        6
        19: iconst_0
        20: istore        8
        22: iload_1
        23: iload_2
        24: iload_3
        25: aload         4
        27: fload         5
        29: dload         6
        31: invokestatic  #7                  // Method add:(BSILjava/lang/String;FD)I
        34: istore        9
        36: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        39: getstatic     #9                  // Field num:I
        42: invokevirtual #10                 // Method java/io/PrintStream.println:(I)V
        45: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        48: iload         9
        50: invokevirtual #10                 // Method java/io/PrintStream.println:(I)V
        53: return
      LineNumberTable:
        line 12: 0
        line 13: 2
        line 14: 4
        line 15: 6
        line 16: 10
        line 17: 14
        line 18: 19
        line 19: 22
        line 20: 36
        line 21: 45
        line 22: 53
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      54     0  args   [Ljava/lang/String;
            2      52     1     a   B
            4      50     2     b   S
            6      48     3     c   I
           10      44     4     d   Ljava/lang/String;
           14      40     5     e   F
           19      35     6     f   D
           22      32     8  bool   Z
           36      18     9     g   I

2.4.4.1、栈帧摘要

// add 方法
  public static int add(byte, short, int, java.lang.String, float, double);
    descriptor: (BSILjava/lang/String;FD)I // 返回值 I 指的是 int 类型
    flags: ACC_PUBLIC, ACC_STATIC // 公共方法、静态方法
    Code:
      stack=4, locals=8, args_size=6 // 栈帧深度是4,最大局部变量8个,入参个数6个

// main 方法
  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V // 返回值 V 指的是 void 类型
    flags: ACC_PUBLIC, ACC_STATIC // 公共方法、静态方法
    Code:
      stack=7, locals=10, args_size=1 // 栈帧深度是7,最大局部变量10个,入参个数1个

2.4.4.2、局部变量表

    局部变量表是一个数组,用来存储当前方法的局部变量,表中可以存储的类型包括boolean、byte、char、short、int、float以及引用类型,因为局部变量是线程私有的,所以不会有线程安全问题,每个栈帧中本地变量表的大小,在.class字节码文件编译完成之后,就已经确定。

// add 方法
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      24     0     a   B
            0      24     1     b   S
            0      24     2     c   I
            0      24     3     d   Ljava/lang/String;
            0      24     4     e   F
            0      24     5     f   D
            6      18     7 d_int   I

// main 方法
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      54     0  args   [Ljava/lang/String; // 入参是 args ,类型是 String 数组
            2      52     1     a   B
            4      50     2     b   S
            6      48     3     c   I
           10      44     4     d   Ljava/lang/String; // 局部变量,类型是 String
           14      40     5     e   F
           19      35     6     f   D
           22      32     8  bool   Z
           36      18     9     g   I

    在 JVM 规范中,每个变量/字段都有描述信息,描述信息主要的作用是描述字段的数据类型,方法的参数列表(包括数量,类型与顺序) 与返回值,根据描述符规则,基本数据类型和代表无返回值的 void 类型都用一个大写的字符来表示的,对象类型则使用字符 L加对象的全限定名称来表示,为了压缩字节码文件的体积。对于基本的数据类型,JVM 都只使用一个大写的字母来表示,如下所示:
标识符

2.4.4.3、操作数栈

# main方法的操作数栈:
         0: iconst_m1                       // 将 int 类型常量 -1 压入栈, 这个就是 a 的值, byte 操作的时候会补齐高位按照 int 值操作
         1: istore_1                        // 将 int 类型值存入局部变量 1 
         2: iconst_2                        // 将 int 类型常量 2 压入栈
         3: istore_2                        // 将 int 类型值存入局部变量 2
         4: iconst_3                        // 将 int 类型常量 3 压入栈
         5: istore_3                        // 将 int 类型值存入局部变量 3
         6: ldc           #3                // String 4, 将常量池中的项压入栈
         8: astore        4                 // 将引用类型值存入局部变量 4
        10: ldc           #4                // float 5.0f, 把常量池中的项压入栈 
        12: fstore        5                 // 将 float 类型值存入局部变量 5
        14: ldc2_w        #5                // double 6.0d, 把常量池中的 double 类型的项压入栈(使用宽索引,long类型也是)
        17: dstore        6					// 将 double 类型值存入局部变量 6
        19: iconst_0						// 将 int 类型常量 0 压入栈,boolean 的值只有两个: true=1, false=0
        20: istore        8                 // 将 int 类型值存入局部变量 8
        22: iload_1							// 从局部变量 1 中装载 int 类型值
        23: iload_2							// 从局部变量 2 中装载 int 类型值
        24: iload_3                         // 从局部变量 3 中装载 int 类型值
        25: aload         4                 // 从局部变量 4 中装载 引用 类型值
        27: fload         5                 // 从局部变量 5 中装载 float 类型值
        29: dload         6					// 从局部变量 6 中装载 double 类型值
        31: invokestatic  #7                // Method add:(BSILjava/lang/String;FD)I, 调用类静态方法 add
        34: istore        9					// 将 int 类型值存入局部变量
        36: getstatic     #8                // Field java/lang/System.out:Ljava/io/PrintStream;, 从类中获取静态字段
        39: getstatic     #9                // Field num:I, 从类中获取静态字段 num 类型为 int
        42: invokevirtual #10               // Method java/io/PrintStream.println:(I)V, 调度对象的实现方法
        45: getstatic     #8                // Field java/lang/System.out:Ljava/io/PrintStream;, 从类中获取静态字段
        48: iload         9					// 从局部变量 9 中装载 int 类型值
        50: invokevirtual #10               // Method java/io/PrintStream.println:(I)V,, 调度对象的实现方法
        53: return							// 从方法中返回,返回值为void

指令的分类:

  • 根据指令的性质,主要可以分为四个类型:
  • 栈操作指令,包括与局部变量交互的指令
  • 程序流程控制指令
  • 对象操作指令,包括方法调用指令
  • 算术运算以及类型转换指令

常用指令:

  • 局部变量压栈指令】将一个局部变量加载到操作数栈:xloadxload_(其中x为i、l、f、d、a,n为0到3)
  • 常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_ml、iconst、lconst、fconst、dconst
  • 出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_(其中x为i、l、f、d、a,n为0到3);xastore(其中x为i、1、f、d、a、b、c、s)

    上面所列举的指令助记符中,有一部分是以尖括号结尾的(例如iload)。这些指令助记符实际上代表了一组指令(例如iload代表了iload0、iload1、iload2和iload3这几个指令)。这几组指令都是某个带有一个操作数的通用指令(例如iload)的特殊形式,对于这若干组特殊指令来说,它们表面上没有操作数,不需要进行取操作数的动作,但操作数都隐含在指令中。

iload_0:将局部变量表中索引为 0 位置上的数据压入操作数栈中。
iload 0:将局部变量表中索引为 0 位置上的数据压入操作数栈中。
iload 4:将局部变量表中索引为 4 位置上的数据压入操作数栈中。

    前两种所表达的意思是相同的,不过iload_0相当于是只有操作码所以只占用1个字节,而iload 0 是操作码和操作数所组成的,而操作数占 2 个字节,所以占用3个字节。

更多指令请参考《JVM 汇编指令 栈和局部变量操作

3、总结

    本文只是简单了解了字节码查看方式、字节码文件的结构、字节码怎么运行的,线程和栈帧之间的关系如下图(网图,帮助理解):

栈和栈帧

最终引用一下网上的线程和栈帧之间的调用过程:

线程创建->线程栈创建->调用栈帧A->栈帧A创建->栈帧A调用操作数栈->栈帧A操作数栈调用栈帧B->栈帧B创建->栈帧B调用操作数栈->栈帧B返回->栈帧B销毁->栈帧A返回->栈帧A销毁->线程栈销毁->线程销毁

  1. 线程创建
  2. 线程栈创建
  3. 调用栈帧A
  4. 栈帧A创建
  5. 栈帧A调用操作数栈
  6. 栈帧A操作数栈调用栈帧B
  7. 栈帧B创建
  8. 栈帧B调用操作数栈
  9. 栈帧B返回
  10. 栈帧B销毁
  11. 栈帧A返回
  12. 栈帧A销毁
  13. 线程栈销毁
  14. 线程销毁

文章引用

https://blog.csdn.net/xidianzxm/article/details/108634553(线程的运行原理-栈帧图解/多线程栈帧)
https://blog.csdn.net/Mr_YarNell/article/details/118482034(Java字节码(Java bytecode))

猜你喜欢

转载自blog.csdn.net/u011047968/article/details/125502318