一、类文件结构

  对 HelloWorld.java 使用 javac 命令进行编译生成 class 文件。-parameters:编译后保留方法参数的名称信息。(参考文献)

javac -parameters -g HelloWorld.java
public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("HelloWorld");
    }
}

  在 Linux 下使用 od 命令读取 class 文件。第一列是标号八进制,后面的列都是十六进制。

[root@localhost ~]# od -t xC HelloWorld.class
0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09
0000020 00 12 00 13 08 00 14 0a 00 15 00 16 07 00 17 07
0000040 00 18 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29
0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e
0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 04 6d 61 69
0000120 6e 01 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67
0000140 2f 53 74 72 69 6e 67 3b 29 56 01 00 10 4d 65 74
0000160 68 6f 64 50 61 72 61 6d 65 74 65 72 73 01 00 04
0000200 61 72 67 73 01 00 0a 53 6f 75 72 63 65 46 69 6c
0000220 65 01 00 0f 48 65 6c 6c 6f 57 6f 72 6c 64 2e 6a
0000240 61 76 61 0c 00 07 00 08 07 00 19 0c 00 1a 00 1b
0000260 01 00 0a 48 65 6c 6c 6f 57 6f 72 6c 64 07 00 1c
0000300 0c 00 1d 00 1e 01 00 1a e9 8d 94 e6 b6 99 e5 a2
0000320 b8 2f 41 2f 6a 76 6d 2f 48 65 6c 6c 6f 57 6f 72
0000340 6c 64 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f
0000360 62 6a 65 63 74 01 00 10 6a 61 76 61 2f 6c 61 6e
0000400 67 2f 53 79 73 74 65 6d 01 00 03 6f 75 74 01 00
0000420 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53
0000440 74 72 65 61 6d 3b 01 00 13 6a 61 76 61 2f 69 6f
0000460 2f 50 72 69 6e 74 53 74 72 65 61 6d 01 00 07 70
0000500 72 69 6e 74 6c 6e 01 00 15 28 4c 6a 61 76 61 2f
0000520 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 56 00 21
0000540 00 05 00 06 00 00 00 00 00 02 00 01 00 07 00 08
0000560 00 01 00 09 00 00 00 1d 00 01 00 01 00 00 00 05
0000600 2a b7 00 01 b1 00 00 00 01 00 0a 00 00 00 06 00
0000620 01 00 00 00 06 00 09 00 0b 00 0c 00 02 00 09 00
0000640 00 00 25 00 02 00 01 00 00 00 09 b2 00 02 12 03
0000660 b6 00 04 b1 00 00 00 01 00 0a 00 00 00 0a 00 02
0000700 00 00 00 09 00 08 00 0a 00 0d 00 00 00 05 01 00
0000720 0e 00 00 00 01 00 0f 00 00 00 02 00 10
0000735

  根据 JVM 规范,类文件结构如下:

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];
}

1、魔数

  魔数,即文件类型,占 0~3 字节,ca fe ba be 表示它是 class 类型的文件。

0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09

2、版本

  4~7 字节,表示类的版本 00 34(52),表示是 Java 8。

0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09

3、常量池

| Constant Type | Value |
| - | - |
| CONSTANT_Class | 7 |
| CONSTANT_Fieldref | 9 |
| CONSTANT_Methodref | 10 |
| CONSTANT_InterfaceMethodref | 11 |
| CONSTANT_String | 8 |
| CONSTANT_Integer | 3 |
| CONSTANT_Float | 4 |
| CONSTANT_Long | 5 |
| CONSTANT_Double | 6 |
| CONSTANT_NameAndType | 12 |
| CONSTANT_Utf8 | 1 |
| CONSTANT_MethodHandle | 15 |
| CONSTANT_MethodType | 16 |
| CONSTANT_InvokeDynamic | 18 |

  8~9 字节,表示常量池长度,00 1f(31)表示常量池有 #1~#30 项,注意 #0 项不计入,也没有值。

0000000 ca fe ba be 00 00 00 34 00 1f 0a 00 06 00 11 09

二、字节码指令

1、反编译

  使用 javap -v xxx.class 对 class 文件进行反编译,-v 表示显示详细信息。

C:\software\IntelliJ IDEA Workspace\algorithm\out\production\algorithm\力扣\A\jvm>javap -v HelloWorld.class
Classfile /C:/software/IntelliJ IDEA Workspace/algorithm/out/production/algorithm/力扣/A/jvm/HelloWorld.class
// 文件最后修改时间,大小(字节)
  Last modified 2021-1-31; size 558 bytes
// 文件的 MD5 签名
  MD5 checksum ba76d7cbd14e48a0d827b449e911bc56
// java 源文件
  Compiled from "HelloWorld.java"
// 类的包名+类名
public class 力扣.A.jvm.HelloWorld
  minor version: 0
// 52 代码 JDK8
  major version: 52
// 类的访问修饰符
  flags: ACC_PUBLIC, ACC_SUPER
// 常量池
Constant pool:
// 方法引用,init 表示构造方法,() 表示无参,V表示无返回值
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
// 引用成员变量
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
// 字符串常量
   #3 = String             #23            // HelloWorld
// 方法引用
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // 力扣/A/jvm/HelloWorld
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               L力扣/A/jvm/HelloWorld;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               HelloWorld.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               HelloWorld
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(Ljava/lang/String;)V
  #26 = Utf8               力扣/A/jvm/HelloWorld
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (Ljava/lang/String;)V
{
// 下面是方法信息
// 类的构造方法
  public 力扣.A.jvm.HelloWorld();
// 方法参数和返回值
    descriptor: ()V
    flags: ACC_PUBLIC
// 代码
    Code:
// 栈的深度,局部变量表的长度,参数的长度
      stack=1, locals=1, args_size=1
// 把局部变量的第0项加载到操作数栈
         0: aload_0
// 调用常量池的第一项方法
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
// 行号表
      LineNumberTable:
// 前面代表 java 源代码的行号,后面是字节码中的行号
        line 6: 0
// 本地变量表
      LocalVariableTable:
// 从字节码开始算的起始范围,作用范围为5,槽位号,变量名,类型
        Start  Length  Slot  Name   Signature
            0       5     0  this   L力扣/A/jvm/HelloWorld;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String HelloWorld
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
// 方法的参数信息
    MethodParameters:
// Flags 表示正常参数
      Name                           Flags
      args
}
SourceFile: "HelloWorld.java"

2、图解方法执行流程

  代码如下:

public class Demo {
  
    public static void main(String[] args) {
        int a = 10;
        int b = Short.MAX_VALUE + 1;
        int c = a + b;
        System.out.println(c);
    }
}

  进行反编译:

C:\software\IntelliJ IDEA Workspace\algorithm\src\力扣\A\jvm\A>javap -v Demo.class
Classfile /C:/software/IntelliJ IDEA Workspace/algorithm/src/力扣/A/jvm/A/Demo.class
  Last modified 2021-1-31; size 481 bytes
  MD5 checksum 840eba3033f3040d568e9c77ab057097
  Compiled from "Demo.java"
public class 鍔涙墸.A.jvm.A.Demo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#18         // java/lang/Object."<init>":()V
   #2 = Class              #19            // java/lang/Short
   #3 = Integer            32768
   #4 = Fieldref           #20.#21        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #22.#23        // java/io/PrintStream.println:(I)V
   #6 = Class              #24            // 鍔涙墸/A/jvm/A/Demo
   #7 = Class              #25            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               MethodParameters
  #15 = Utf8               args
  #16 = Utf8               SourceFile
  #17 = Utf8               Demo.java
  #18 = NameAndType        #8:#9          // "<init>":()V
  #19 = Utf8               java/lang/Short
  #20 = Class              #26            // java/lang/System
  #21 = NameAndType        #27:#28        // out:Ljava/io/PrintStream;
  #22 = Class              #29            // java/io/PrintStream
  #23 = NameAndType        #30:#31        // println:(I)V
  #24 = Utf8               鍔涙墸/A/jvm/A/Demo
  #25 = Utf8               java/lang/Object
  #26 = Utf8               java/lang/System
  #27 = Utf8               out
  #28 = Utf8               Ljava/io/PrintStream;
  #29 = Utf8               java/io/PrintStream
  #30 = Utf8               println
  #31 = Utf8               (I)V
{
  public 鍔涙墸.A.jvm.A.Demo();
    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 6: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: bipush        10
         2: istore_1
         3: ldc           #3                  // int 32768
         5: istore_2
         6: iload_1
         7: iload_2
         8: iadd
         9: istore_3
        10: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_3
        14: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
        17: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 6
        line 12: 10
        line 13: 17
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "Demo.java"
(1)常量池载入运行时常量池

  当 Java 代码被执行时,由 JVM 的类加载器,对 main 方法所在的类,进行类加载。类加载就是把 class 字节数据读取到内存中。常量池中的数据会放在运行时常量池中,运行时常量池实际在方法区中。在 java 中,比较小的数字存储在方法的字节码指令中,当数值大于 short 的最大值时,其存储在常量池中。

P3HO2LMFP2O46S3WTWI.png

(2)方法字节码载入方法区

30S4R5MQ0DK71YBZ48.png

(3)main 线程开始运行,分配栈帧内存

  绿色的为局部变量表,蓝色的为操作数栈。(stack=2,locals=4) 对应操作数栈有2个空间(每个空间 4 个字节),局部变量表中有 4 个槽位。

IT46383XSP63LO2EZO6X.png

(4)执行引擎开始执行字节码

bipush 10:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有:

  • sipush:将一个 short 压入操作数栈(其长度会补齐 4 个字节);
  • ldc:将一个 int 压入操作数栈;
  • ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节);
  • 这里小的数字都是和字节码指令存在一起,超过 short 范围的数字存入了常量池。

1.png

istore_1:将操作数栈顶数据弹出,存入局部变量表的 slot 1(槽位)。

QUI6ZS3ETFCJKTSCVAC.png

VIU9PJD69323GSPUXHL.png

idc #3:从常量池加载 #3 数据到操作数栈。注意:Short.MAX_VALUE 是 32767,所以 32768 = Short.MAX_VALUE + 1 实际是在编译期间计算好的。

1.png

istore_2:将操作数栈顶数据弹出,存入局部变量表的 slot 2(槽位)。

3FUA2NPOX6O2O3ZM.png

I0R89K0FLIFEYCR2J3.png

iload_1:接着执行 a+b 的操作,运算操作需要在操作数栈里操作,所以先对局部变量表进行读取。

099J3JGMRZ3TJNKF.png

iload_2:读取局部变量表的 slot 2,压入操作数栈中。

G92UA06TXLSYIVK2R.png

iadd:弹出操作数栈的两个变量,对其进行相加后再入栈。

PJBHIYWEBYP2T1WX93.png

EJNWHVXPO5CL6I6BEQ.png

istore_3:将操作数栈顶数据弹出,存入局部变量表的 slot 3(槽位)。

GY3KVNDXL32V9X0.png

getstatic #4:去常量池中寻找成员变量引用,对象实际存储在堆中,将对象的引用放入操作数栈中。

MV2TY7ASQ96YBJP.png

NCMTP30DX3QJGAZTQ.png

iload_3:调用方法需要参数,所以先将参数入栈。

X05GTFZLLQRWKBFBHVPAC.png

41T3SWCC3WW95T.png

invokevirtual #5

  • 找到常量池 #5 项;
  • 定位到方法去java/io/PrintStream.println:(I)V 方法;
  • 生成新的栈帧(分配 locals、stack 等);
  • 传递参数。执行新栈帧的字节码。

2OVXT9DLQ1VY4UGRPU8.png

  • 执行完毕,弹出栈帧;
  • 清除 main 操作数栈内容。

U0LLOVBNWHOYFU2.png

return

  • 完成 main 方法调用,弹出 main 栈帧;
  • 程序结束。

3、通过字节码指令来分析代码

(1)i++

  代码如下:

public class Demo {

    public static void main(String[] args) {
        int a = 10;
        int b = a++ + ++a + a--;
        System.out.println(a);
        System.out.println(b);
    }
}

  反编译后的代码部分:

Code:
      stack=2, locals=3, args_size=1
	// 将 10 压入栈
         0: bipush        10
	// 将 10 弹出栈,放入局部变量表 1
         2: istore_1
	// 将局部变量 1 入栈
         3: iload_1
	// 参数一代表局部变量的位置(槽位),参数二代表自增几
         4: iinc          1, 1
         7: iinc          1, 1
        10: iload_1
        11: iadd
        12: iload_1
        13: iinc          1, -1
        16: iadd
        17: istore_2
        18: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintSt
ream;
        21: iload_1
        22: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        25: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintSt
ream;
        28: iload_2
        29: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        32: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 18
        line 12: 25
        line 13: 32
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      33     0  args   [Ljava/lang/String;
            3      30     1     a   I
           18      15     2     b   I

  分析:a++ 先 iload 后 iinc(自增),++a 相反。注意 iinc 指令是直接在局部变量 slot 上进行运算。

(2)条件判断指令

| 指令 | 助记符 | 含义 |
| - | - | - |
| 0x99 | ifeq | 判断是否 == 0 |
| 0x9a | ifne | 判断是否 != 0 |
| 0x9b | iflt | 判断是否 < 0 |
| 0x9c | ifge | 判断是否 >= 0 |
| 0x9d | ifgt | 判断是否 > 0 |
| 0x9e | ifle | 判断是否 <= 0 |
| 0x9f | if_icmpeq | 两个int是否 == |
| 0xa0 | if_icmpne | 两个int是否 != |
| 0xa1 | if_icmplt | 两个int是否 < |
| 0xa2 | if_icmpge | 两个int是否 >= |
| 0xa3 | if_icmpgt | 两个int是否 > |
| 0xa4 | if_icmple | 两个int是否 <= |
| 0xa5 | if_acmpeq | 两个引用是否 == |
| 0xa6 | if_acmpne | 两个引用是否 != |
| 0xc6 | ifnull | 判断是否 == null |
| 0xc7 | ifnonnull | 判断是否 != null |

  以上比较指令中没有 long,float,double 的比较,详见(官方文档)

  • byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节;
  • goto 用来进行跳转到指定行号的字节码。

  代码如下:

public class Demo {

    public static void main(String[] args) {
        int a = 0;
        if (a == 0) {
            a = 10;
        } else {
            a = 20;
        }
    }
}

  字节码如下:

// -1 ~ 5 之间的数用 iconst 表示
         0: iconst_0
	// 将其放入局部变量表
         1: istore_1
	// 将其放入栈
         2: iload_1
	// 判断该数是否 == 0,不等于 0 则跳到 12 行
         3: ifne          12
	// 将 10 压入栈
         6: bipush        10
         8: istore_1
         9: goto          15
	// 将 20 压入栈 
        12: bipush        20
        14: istore_1
        15: return
(3)x = x++

  代码如下:

public class Demo {

    public static void main(String[] args) {
        int i = 0;
        int x = 0;
        while (i < 10) {
            x = x++;
            i++;
        }
        // x 为 0
        System.out.println(x);
    }
}

  字节码如下:

0: iconst_0
         1: istore_1
         2: iconst_0
         3: istore_2
         4: iload_1
         5: bipush        10
         7: if_icmpge     21
        10: iload_2
        11: iinc          2, 1
        14: istore_2
        15: iinc          1, 1
        18: goto          4
        21: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintSt
ream;
        24: iload_2
        25: invokevirtual #3                  // Method java/io/PrintStream.println:(I)V
        28: return

  分析:每次循环都是先将 x 的值压入栈,然后对局部变量表中的 x 自增,接着将栈中的值弹出,放入局部变量表中,等于没有做自增操作。

4、构造方法

(1)cinit

  <cinit>()V 方法是类的构造方法,该方法会在类加载的初始化阶段被调用。

public class Demo {

    static int i = 0;

    static {
        i = 20;
    }

    static {
        i = 30;
    }
}

  编译器会按从上至下的顺序,收集所有 static 静态代码和静态成员赋值的代码,合并为一个特殊的方法 <cinit>()V。如以下字节码:

// 将数字 10 入栈
         0: bipush        10
	// 将操作数栈的 10 赋值给常量池的第 2 项
         2: putstatic     #2                  // Field i:I
         5: bipush        20
         7: putstatic     #2                  // Field i:I
        10: bipush        30
        12: putstatic     #2                  // Field i:I
        15: return
(2)init

  <init>()V 方法是实例对象的构造方法。

public class Demo {

    private String a = "s1";

    {
        b = 20;
    }

    private int b = 10;

    {
        a = "s2";
    }

    public Demo(String a, int b) {
        this.a = a;
        this.b = b;
    }

    public static void main(String[] args) {
        Demo demo = new Demo("s3", 30);
        System.out.println(demo.a);
        System.out.println(demo.b);
    }
}

  编译器会按从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在最后。

public 鍔涙墸.A.jvm.A.Demo(java.lang.String, int);
    descriptor: (Ljava/lang/String;I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=3
	// 获取 this
         0: aload_0
	// 调用父类的构造方法
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
	// 将 s1 压入栈
         5: ldc           #2                  // String s1
	// 给 this.a 赋值 s1
         7: putfield      #3                  // Field a:Ljava/lang/String;
        10: aload_0
        11: bipush        20
	// 给 this.b 赋值 20
        13: putfield      #4                  // Field b:I
        16: aload_0
        17: bipush        10
	// 给 this.b 赋值 10
        19: putfield      #4                  // Field b:I
        22: aload_0
        23: ldc           #5                  // String s2
	// 给 this.a 赋值 s2
        25: putfield      #3                  // Field a:Ljava/lang/String;
	// 开始执行原构造方法
        28: aload_0
        29: aload_1
        30: putfield      #3                  // Field a:Ljava/lang/String;
        33: aload_0
        34: iload_2
        35: putfield      #4                  // Field b:I
        38: return

5、方法调用

  代码如下:

public class Demo {

    public Demo() {
    }

    private void test1() {
    }

    private final void test2() {
    }

    public void test3() {
    }

    public static void test4() {
    }

    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.test1();
        demo.test2();
        demo.test3();
        demo.test4();
        Demo.test4();
    }
}

  jvm 调用不同类型的方法,使用不同的字节码指令。invokespecial 和 invokestatic 属于静态绑定,它们在字节码指令生成时,就知道如果找到该方法。invokevirtual 是动态绑定,不能确定运行时调用的是哪个对象的方法,也许是子类的,也行是父类的。

  • 私有、构造、被 final 修饰的方法,在调用时都使用 invokespecial 指令;
  • 普通成员方法在调用时,使用 invokevirtual 指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定;
  • 静态方法在调用时使用 invokestatic 指令。

  字节码如下:

// 为对象在堆空间分配内存,分配成功会把对象的引用返回操作数栈
         0: new           #2                  // class 鍔涙墸/A/jvm/A/Demo
	// dup 复制栈顶的内容,现在栈里有两个对象的引用
         3: dup
	// invokespecial 将栈顶的引用弹出,调用方法(执行构造方法,消耗的是复制的引用)
         4: invokespecial #3                  // Method "<init>":()V
	// 将栈顶的引用弹出(对象的引用,this)存入局部变量表
         7: astore_1
         8: aload_1
	// 私有方法
         9: invokespecial #4                  // Method test1:()V
        12: aload_1
	// final 
        13: invokespecial #5                  // Method test2:()V
        16: aload_1
	// 普通的 public 方法
        17: invokevirtual #6                  // Method test3:()V
        20: aload_1
	// 调用静态方法不需要对象引用,所以把栈顶的对象引用出栈。
        21: pop
	// 调用静态方法
        22: invokestatic  #7                  // Method test4:()V
        25: invokestatic  #7                  // Method test4:()V
        28: return

6、多态原理

  Java 虚方法:所有被 overriding 的方法都是 virtual 的,所有重写的方法都是 override 的。因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令。在执行 invokevirtual 指令时,经历了以下几个步骤:

  • 先通过栈帧中对象的引用找到对象;
  • 分析对象头,找到对象实际的 Class;
  • Class 结构中有 vtable(虚方法表),它在类加载的链接阶段就已经根据方法的重写规则生成好了;
  • 查询 vtable 找到方法的具体地址;
  • 执行方法的字节码。

  可以使用 HSDB 查看 JVM 运行时期的数据。

java -cp C:\software\Java\jdk1.8.0_211\lib\sa-jdi.jar sun.jvm.hotspot.HSDB

7、异常处理

(1)try-catch
public class Demo {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        }
    }
}

  字节码:

  • 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号。
  • 第 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 的位置。
0: iconst_0
	// 将 0 放入局部变量表
         1: istore_1
         2: bipush        10
         4: istore_1
	// 没有发生异常就跳到 12 行
         5: goto          12
	// 将储异常对象到局部变量表
         8: astore_2
         9: bipush        20
        11: istore_1
        12: return
      Exception table:
	// 起始行 结束行 异常后跳转行 异常类型
         from    to  target type
             2     5     8   Class java/lang/Exception
      LineNumberTable:...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/Exception;
            0      13     0  args   [Ljava/lang/String;
            2      11     1     i   I
(2)多个 single-catch
public class Demo {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException e) {
            i = 30;
        } catch (NullPointerException e) {
            i = 40;
        } catch (Exception e) {
            i = 50;
        }
    }
}

  多个异常共用一个槽位,因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用。

Code:
      stack=1, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        10
         4: istore_1
         5: goto          26
         8: astore_2
         9: bipush        30
        11: istore_1
        12: goto          26
        15: astore_2
        16: bipush        40
        18: istore_1
        19: goto          26
        22: astore_2
        23: bipush        50
        25: istore_1
        26: return
      Exception table:
	// 都是检测 [2,5) 行代码
         from    to  target type
             2     5     8   Class java/lang/ArithmeticException
             2     5    15   Class java/lang/NullPointerException
             2     5    22   Class java/lang/Exception
      LineNumberTable:...
	// 多个异常共用一个槽位
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            9       3     2     e   Ljava/lang/ArithmeticException;
           16       3     2     e   Ljava/lang/NullPointerException;
           23       3     2     e   Ljava/lang/Exception;
            0      27     0  args   [Ljava/lang/String;
            2      25     1     i   I

  multi-catch 和 多个 single-catch 字节码原理一样。

public class Demo {

    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (ArithmeticException | NullPointerException e) {
            i = 20;
        }
    }
}
(3)finally
public class Demo {
  
    public static void main(String[] args) {
        int i = 0;
        try {
            i = 10;
        } catch (Exception e) {
            i = 20;
        } finally {
            i = 30;
        }
    }
}

  可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程。注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次。

Code:
      stack=1, locals=4, args_size=1
         0: iconst_0
         1: istore_1
	// try 块
         2: bipush        10
         4: istore_1
	// try 块执行完后,执行 finally 块
         5: bipush        30
         7: istore_1
         8: goto          27
	// catch 块
	// 将异常信息放入局部变量表的 2 号槽位
        11: astore_2
        12: bipush        20
	// catch 块执行完后,执行 finally 块
        14: istore_1
        15: bipush        30
        17: istore_1
        18: goto          27
	// 出现异常,但未被捕获,会抛出其他异常,也需要执行 finally 块
        21: astore_3
        22: bipush        30
        24: istore_1
        25: aload_3
	// 抛异常
        26: athrow
        27: return
      Exception table:
         from    to  target type
             2     5    11   Class java/lang/Exception
             2     5    21   any // 剩余的异常类型,比如 Error
            11    15    21   any // 剩余的异常类型,比如 Error
      LineNumberTable:...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           12       3     2     e   Ljava/lang/Exception;
            0      28     0  args   [Ljava/lang/String;
            2      26     1     i   I

finally 出现了 return

public class Demo {

    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {
        try {
            return 10;
        } finally {
            return 20;
        }
    }
}

  代码运行结果为 20。由于 finally 中的 ireturn 被插入了所有可能的流程,因此返回结果肯定以 finally 的为准,所以无论 try 流程中是否发生异常都会返回 20。

Code:
      stack=1, locals=2, args_size=0
         0: bipush        10
	// 将 10 放入局部变量表中
         2: istore_0
	// 执行 try 中的 finally
         3: bipush        20
	// 返回栈顶值 20
         5: ireturn
	// 执行 catch 剩余的异常类型的 finally
	// 发生异常后,将异常对象放入局部变量表
         6: astore_1
         7: bipush        20
	// 所以不管 try 中有没有异常都会返回 20 
         9: ireturn
      Exception table:
         from    to  target type
             0     3     6   any

finally 中的 return 吞异常

  将上例和上上例中的 finally 相比,发现没有 athrow 了,这告诉我们:如果在 finally 中出现了 return,会吞掉异常。所以不要在 finally 中进行 return 操作。

public class Demo {

    public static void main(String[] args) {
        int result = test();
        // 打印结果仍然是 20,并未抛出异常
        System.out.println(result);
    }

    public static int test() {
        try {
            int i = 1 / 0;
            return 10;
        } finally {
            return 20;
        }
    }
}

finally 对返回值的影响

  上上例的字节码中第 2 行,似乎没啥用,看以下代码:

public class Demo {

    public static void main(String[] args) {
        int result = test();
        System.out.println(result);
    }

    public static int test() {
        int i = 10;
        try {
            return i;
        } finally {
            i = 20;
        }
    }
}

  运行结果是 10,return 结果是另存的,并不受变量值的改变。

Code:
      stack=1, locals=3, args_size=0
	// 将 10 压入栈
         0: bipush        10
	// 把 10 赋值给 i
         2: istore_0
	// 把 i 加载到栈中
         3: iload_0
	// 将栈顶元素放入局部变量表 1 的位置,用来固定返回值
         4: istore_1
	// 执行 finally
	// 把 20 入栈
         5: bipush        20
	// 给 i 赋值
         7: istore_0
	// 把局部变量表 1 的值入栈
         8: iload_1
	// 返回栈顶值 10
         9: ireturn
	// 如果发生异常执行
        10: astore_2
	// 执行 finally
        11: bipush        20
        13: istore_0
	// 加载异常
        14: aload_2
	// 抛出异常
        15: athrow
      Exception table:
         from    to  target type
             3     5    10   any
      LineNumberTable:...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            3      13     0     i   I

8、Synchronized

public class Demo {

    public static void main(String[] args) {
        Object lock = new Object();
        synchronized (lock) {
            System.out.println("ok");
        }
    }
}

  字节码如下:(方法级别的 Synchronized 不会在字节码指令中体现)

Code:
      stack=2, locals=4, args_size=1
         0: new           #2                  // class java/lang/Object
	// 复制对象引用到栈顶,用于构造函数消耗
         3: dup
         4: invokespecial #1                  // Method java/lang/Object."<init>":()V
	// 剩下的一份对象引用存放到局部变量表的 1 号槽位
         7: astore_1
	// 将局部变量表 1 号槽位的值,加载到栈中
         8: aload_1
	// 复制一份,放到栈顶,用于加锁时消耗
         9: dup
	// 将栈顶元素弹出,存到局部变量表 2 号槽位。栈中剩一份对象的引用
        10: astore_2
	// 加锁
        11: monitorenter
	// 锁住后面代码块
        12: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        15: ldc           #4                  // String ok
        17: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
	// 加载局部变量表 2 号槽位的对象引用,用于解锁
        20: aload_2
	// 解锁
        21: monitorexit
        22: goto          30
	// 如果发生异常,则也需要执行解锁
        25: astore_3
        26: aload_2
	// 解锁
        27: monitorexit
        28: aload_3
	// 抛异常
        29: athrow
        30: return
      Exception table:
	// 保证解锁能够执行
         from    to  target type
            12    22    25   any
            25    28    25   any
      LineNumberTable:...
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1  lock   Ljava/lang/Object;

三、编译器处理

  所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利。

  注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。

1、默认构造器

public class Candy1 {

}

  编译后 class 后的代码:

public class Candy1 {
   // 无参构造器是 java 编译器帮我们加上的
   public Candy1() {
      // 调用父类 Object 的无参构造方法,即调用 java/lang/Object." <init>":()V
      super();
   }
}

2、自动拆装箱

  基本类型和其包装类型的相互转换过程,称为拆装箱。在 JDK 5 以后,它们的转换可以在编译期自动完成。

public class Demo2 {
   public static void main(String[] args) {
      Integer x = 1;
      int y = x;
   }
}

  在 JDK 5 之前必须改为一下代码,才能编译通过:

public class Demo2 {
   public static void main(String[] args) {
      // 基本类型赋值给包装类型,称为装箱
      Integer x = Integer.valueOf(1);
      // 包装类型赋值给基本类型,称谓拆箱
      int y = x.intValue();
   }
}

3、泛型集合取值

  泛型也是在 JDK 5 开始加入的特性,但 java 在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:

public class Candy {

    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        // 实际调用的是 List.add(Object e)
        list.add(10);
        Integer x = list.get(0);
    }
}

  对应字节码:

Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class java/util/ArrayList
         3: dup
         4: invokespecial #3                  // Method java/util/ArrayList."<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        10
        11: invokestatic  #4                  // Method java/lang/Integer.valueOf:(I)Ljava/lan
g/Integer;
        // 这里进行了泛型擦除,实际调用的是add(Objcet o)
        14: invokeinterface #5,  2            // InterfaceMethod java/util/List.add:(Ljava/lan
g/Object;)Z
        19: pop
        20: aload_1
        21: iconst_0
        // 这里也进行了泛型擦除,实际调用的是get(Object o) 
        22: invokeinterface #6,  2            // InterfaceMethod java/util/List.get:(I)Ljava/l
ang/Object;
        // 这里进行了类型转换,将Object转换成了Integer
        27: checkcast     #7                  // class java/lang/Integer
        30: astore_2
        31: return
      LineNumberTable:
        line 15: 0
        line 17: 8
        line 18: 20
        line 19: 31
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      32     0  args   [Ljava/lang/String;
            8      24     1  list   Ljava/util/List;
           31       1     2     x   Ljava/lang/Integer;
      // 擦除的是字节码上的泛型信息,LocalVariableTypeTable 仍然保留了方法参数泛型的信息。
      // 只有方法参数和返回值上的信息可以通过反射获取
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            8      24     1  list   Ljava/util/List<Ljava/lang/Integer;>;

  所以在取值时,编译器真正生成的字节码中,有一个类型转换的操作:

Integer x = (Integer) list.get(0);

  如果要将返回结果赋值给一个 int 类型的变量,则还有自动拆箱的操作:

int x = ((Integer) list.get(0)).intValue();

4、可变参数

  JDK 5 开始加入的新特性:

public class Demo4 {
   public static void foo(String... args) {
      // 将 args 赋值给 arr,可以看出 String...实际就是 String[] 
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo("hello", "world");
   }
}

  可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。同样 java 编译器会在编译期间将上述代码变换为:

public class Demo4 {
   public Demo4 {}

   public static void foo(String[] args) {
      String[] arr = args;
      System.out.println(arr.length);
   }

   public static void main(String[] args) {
      foo(new String[]{"hello", "world"});
   }
}

  注意,如果调用的是foo(),则等价代码为 foo(new String[]{}),创建了一个空数组,而不是直接传递的 null。

5、foreach

public class Demo5 {
    public static void main(String[] args) {
        // 数组赋初值的简化写法也是一种语法糖。
        int[] arr = {1, 2, 3, 4, 5};
        for (int x : arr) {
            System.out.println(x);
        }
    }
}

  编译器会转换为:

public class Demo5 {
    public Demo5() {
    }

    public static void main(String[] args) {
        int[] arr = new int[]{1, 2, 3, 4, 5};
        for (int i = 0; i < arr.length; ++i) {
            int x = arr[i];
            System.out.println(x);
        }
    }
}

如果是集合使用 foreach

public class Demo5 {
   public static void main(String[] args) {
      List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
      for (Integer x : list) {
         System.out.println(x);
      }
   }
}

  集合要使用 foreach,需要该集合类实现了 Iterable接口,因为集合的遍历需要用到迭代器 Iterator。

public class Demo5 {
    public Demo5 {}
  
   public static void main(String[] args) {
      List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
      // 获得该集合的迭代器
      Iterator<Integer> iterator = list.iterator();
      while(iterator.hasNext()) {
         Integer x = (Integer)iterator.next();
         System.out.println(x);
      }
   }
}

6、switch 字符串

  从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实就是语法糖。

public class Demo6 {
   public static void main(String[] args) {
      String str = "hello";
      switch (str) {
         case "hello" :
            System.out.println("h");
            break;
         case "world" :
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

过程说明:

  • 在编译期间,单个的 switch 被分为了两个:
    • 第一个用来匹配字符串,并给 x 赋值;
      • 字符串的匹配用到了字符串的 hashCode,还用到了 equals 方法;
      • 使用 hashCode 是为了提高比较效率,使用 equals 是防止有 hashCode 冲突(如:BM 和 C.)
    • 第二个用来根据 x 的值来决定输出语句。
  • switch 配合 String 和枚举使用时,变量不能为 null,否则在调用 hashCode() 时会报空指针异常。
public class Demo6 {
   public Demo6() {
   }

   public static void main(String[] args) {
      String str = "hello";
      int x = -1;
      // 通过字符串的 hashCode+value 来判断是否匹配
      switch (str.hashCode()) {
         // hello 的 hashCode
         case 99162322 :
            // 再次比较,因为字符串的 hashCode 有可能出现 hash 冲突
            if(str.equals("hello")) {
               x = 0;
            }
            break;
         // world 的 hashCode
         case 11331880 :
            if(str.equals("world")) {
               x = 1;
            }
            break;
         default:
            break;
      }

      // 用第二个 switch 在进行输出判断
      switch (x) {
         case 0:
            System.out.println("h");
            break;
         case 1:
            System.out.println("w");
            break;
         default:
            break;
      }
   }
}

7、switch枚举

public class Demo7 {

   public static void main(String[] args) {
      SEX sex = SEX.MALE;
      switch (sex) {
         case MALE:
            System.out.println("man");
            break;
         case FEMALE:
            System.out.println("woman");
            break;
         default:
            break;
      }
   }
}

enum SEX {
   MALE, FEMALE;
}

编译器中执行的代码如下

public class Demo7 {
   /**   
    * 定义一个合成类(仅 jvm 使用,对我们不可见)   
    * 用来映射枚举的 ordinal 与数组元素的关系   
    * 枚举的 ordinal 表示枚举对象的序号,从 0 开始   
    * 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1   
    */ 
   static class $MAP {
      // 数组大小即为枚举元素个数,里面存放了case 用于比较的数字
      static int[] map = new int[2];
      static {
         // ordinal 即枚举元素对应所在的位置,MALE为0,FEMALE为1
         map[SEX.MALE.ordinal()] = 1;
         map[SEX.FEMALE.ordinal()] = 2;
      }
   }

   public static void main(String[] args) {
      SEX sex = SEX.MALE;
      // 将对应位置枚举元素的值赋给 x,用于 case 操作
      int x = $MAP.map[sex.ordinal()];
      switch (x) {
         case 1:
            System.out.println("man");
            break;
         case 2:
            System.out.println("woman");
            break;
         default:
            break;
      }
   }
}

enum SEX {
   MALE, FEMALE;
}

8、枚举类

  JDK 7 新增了枚举类。

enum Sex {
   MALE, FEMALE;
}

转换后的代码

public final class Sex extends Enum<Sex> {   
   // 对应枚举类中的元素
   public static final Sex MALE;  
   public static final Sex FEMALE;  
   private static final Sex[] $VALUES;
   
    static {   
    	// 调用构造函数,传入枚举元素的值及 ordinal
    	MALE = new Sex("MALE", 0);  
        FEMALE = new Sex("FEMALE", 1);   
        $VALUES = new Sex[]{MALE, FEMALE}; 
   }
 
   // 调用父类中的方法
    private Sex(String name, int ordinal) {   
        super(name, ordinal);  
    }
   
    public static Sex[] values() {  
        return $VALUES.clone();  
    }
    public static Sex valueOf(String name) { 
        return Enum.valueOf(Sex.class, name);  
    } 
   
}

9、try-with-resources

  JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources:

try(资源变量 = 创建资源对象){
} catch() {
}

  其中资源对象需要实现 AutoCloseable 接口,例如 InputStream、OutputStream、Connection、Statement、ResultSet 等接口都实现了 AutoCloseable,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:

public class Candy9 {

    public static void main(String[] args) {
        try (InputStream is = new FileInputStream("d:\\1.txt")) {
            System.out.println(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

  会被转换为:

public class Candy9 {
    public Candy9() {
    }

    public static void main(String[] args) {
        try {
            InputStream is = new FileInputStream("d:\\1.txt");
            Throwable var2 = null;
            try {
                System.out.println(is);
            } catch (Throwable var12) {
                // var2 是我们代码出现的异常
                var2 = var12;
                throw var12;
            } finally {
                // 判断资源是否为空
                if (is != null) {
                    // 判断代码是否有异常
                    if (var2 != null) {
                        try {
                            is.close();
                        } catch (Throwable var11) {
                            // 如果 close 出现异常,作为被压制异常添加,这样两个异常都不会丢失
                            var2.addSuppressed(var11);
                        }
                    } else {
                        // 如果代码中没有异常,close 出现的异常就是最好 catch 块中的 var14
                        is.close();
                    }
                }

            }
        } catch (IOException var14) {
            var14.printStackTrace();
        }

    }
}

  为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常):

public class Candy9 {
    public static void main(String[] args) {
        try (MyResource resource = new MyResource()) {
            int i = 1 / 0;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

class MyResource implements AutoCloseable {

    // 让其在关闭资源时,去抛异常
    public void close() throws Exception {
        throw new Exception("close 异常");
    }
}

  输出(两个异常信息都不会丢失):

java.lang.ArithmeticException: / by zero
	at B.A.jvm.Candy9.main(Candy9.java:20)
	Suppressed: java.lang.Exception: close 异常
		at B.A.jvm.MyResource.close(Candy9.java:29)
		at B.A.jvm.Candy9.main(Candy9.java:21)

10、方法重写时的桥接方法

  方法重写时对返回值分两种情况:

  • 父子类的返回值完全一致;
  • 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子);
class A {
    public Number m() {
        return 1;
    }
}

class B extends A {

    @Override
    // 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
    public Integer m() {
        return 2;
    }
}

  对于子类,java 编译器会做如下处理:

class B extends A {

    @Override
    public Integer m() {
        return 2;
    }

    // 此方法才是真正重写了父类 public Number m() 方法
    public synthetic bridge Number m() {
        // 调用 public Integer m()
        return m();
    }
}

  Number m()是 jvm 生成的合成方法,对程序员不可见,该方法作为桥接方法。所以才能实现子类返回值可以是父类返回值的子类。该方法可以通过反射代码来查看。

11、匿名内部类

public class Demo8 {
   public static void main(String[] args) {
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println("running...");
         }
      };
   }
}

  转换后的代码:

public class Demo8 {
   public static void main(String[] args) {
      // 用额外创建的类来创建匿名内部类对象
      Runnable runnable = new Demo8$1();
   }
}

// 创建了一个额外的类,实现了Runnable接口
final class Demo8$1 implements Runnable {
   public Demo8$1() {}

   @Override
   public void run() {
      System.out.println("running...");
   }
}

  如果匿名内部类中引用了局部变量

public class Demo8 {
   public static void main(String[] args) {
      final int x = 1;
      Runnable runnable = new Runnable() {
         @Override
         public void run() {
            System.out.println(x);
         }
      };
   }
}

  转换后的代码:

public class Demo8 {
   public static void main(String[] args) {
      final int x = 1;
      // 用额外创建的类来创建匿名内部类对象
      Runnable runnable = new Demo8$1(x);
   }
}

final class Demo8$1 implements Runnable {
   // 多创建了一个变量
   int val$x;
   // 变为了有参构造器
   public Demo8$1(int x) {
      this.val$x = x;
   }

   @Override
   public void run() {
      System.out.println(val$x);
   }
}

  注意:这解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建 Demo8$1 对象时,将 x 的值赋值给了 Demo8$1 对象的 val$x 属性,不应该再发生变了,如果变,那么 x 属性没有机会再跟着一起变化。

  以下代码无法编译通过:

public class Demo8 {
    public static void main(String[] args) {
        int x = 1;
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println(x);
            }
        };
        x = 2;
    }
}

标题:JVM 字节码技术
作者:Yi-Xing
地址:http://47.94.239.232/articles/2021/01/30/1612018889279.html
博客中若有不恰当的地方,请您一定要告诉我。前路崎岖,望我们可以互相帮助,并肩前行!