深入解析“Java 字节码 ” 之 「进一步探究 Java 方法的字节码实现」

Posted by tomas家的小拨浪鼓 on November 3, 2019

深入解析“Java 字节码 ” 之 「进一步探究 Java 方法的字节码实现」

本文主要是《深入探索 JVM》系列中『字节码篇』文章,主要对 Java 方法的字节码实现进行了进一步的深入介绍。系列文章目录见:《 深入探索 JVM 》文集


『字节码』篇文章推荐:
深入解析“Java 字节码 ” 之 「类文件结构」
深入解析“Java 字节码 ” 之 「从案例深度解读 Java 字节码」
深入解析“Java 字节码 ” 之 「进一步探究 Java 方法的字节码实现」
深入解析“Java 字节码 ” 之 「从案例解读虚拟机属性表」
深入解析“Java 字节码 ” 之 「虚拟机字节码执行引擎」
深入解析“Java 字节码 ” 之 「动态代理的实现」


一,synchronized 关键字所生成的字节码

1.1 synchronized 代码块

private void test(String str) {
    synchronized (object) {
        System.out.println("hello world");
    }
}

字节码:

  private void test(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PRIVATE
    Code:
      stack=2, locals=4, args_size=2
         0: aload_0
         1: getfield      #6                  // Field object:Ljava/lang/Object;
         4: dup
         5: astore_2
         6: monitorenter
         7: getstatic     #12                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc           #13                 // String hello world
        12: invokevirtual #14                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        15: aload_2
        16: monitorexit                        // 方法正常执行,退出同步块
        17: goto          25
        20: astore_3
        21: aload_2
        22: monitorexit                        // 方法执行异常,以抛异常的方式离开的同步快
        23: aload_3
        24: athrow
        25: return
      Exception table:
         from    to  target type
             7    17    20   any
            20    23    20   any
      LineNumberTable:
        line 28: 0
        line 29: 7
        line 30: 15
        line 31: 25
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      26     0  this   Lcom/bayern/shengsiyuan/jvm_lecture/bytecode/lesson13/MyTest2;
            0      26     1   str   Ljava/lang/String;
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 20
          locals = [ class com/bayern/shengsiyuan/jvm_lecture/bytecode/lesson13/MyTest2, class java/lang/String, class java/lang/Object ]
          stack = [ class java/lang/Throwable ]
        frame_type = 250 /* chop */
          offset_delta = 4

字节码对「synchronized 代码块」是通过「monitorenter」和「monitorexit」指令来实现的锁的获取与释放动作。

当线程进入到 monitorenter 指令后,线程将会持有 Monitor 对象,退出 monitorenter 指令后(即,执行了 monitorexit 指令后),线程将会释放 Monitor 对象。

“monitorenter”与“monitorexit”并不是一对一的关系,可能是一对一,也可能是一对多。比如,👆的示例。

  • Q:为什么👆method方法的字节码中,有两个“monitorexit”了?
    A:因为,对于synchronized中的代码块,无论是正常的执行完,还是执行的过程遇到了运行时异常而抛出异常的退出,都需要保证这个线程能够释放掉这个对象的锁。 其中,「16: monitorexit // 方法正常执行,退出同步块」而,「22: monitorexit // 方法执行异常,以抛异常的方式离开的同步快」


1.2 synchronized 实例方法

private synchronized void setX(int x) {
    this.x = x;
}

字节码:

  private synchronized void setX(int);
    descriptor: (I)V
    flags: ACC_PRIVATE, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: iload_1
         2: putfield      #4                  // Field x:I
         5: return
      LineNumberTable:
        line 22: 0
        line 23: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/bayern/shengsiyuan/jvm_lecture/bytecode/lesson13/MyTest2;
            0       6     1     x   I

对于 synchronized 关键字修饰方法来说,并没有出现 monitorenter 与 monitorexit 指令,而是出现了一个 ACC_SYNCHRONIZED 标志。

JVM 使用了 ACC_SYNCHRONIZED 访问标志来区分一个方法是否为同步方法;当方法被调用的时候,调用指令会检查该方法是否拥有 ACC_SYNCHRONIZED 标志。如果有,那么执行线程将会先持有方法所在对象的 Monitor 对象,然后再去执行方法体;在该方法执行期间,其他任何线程均无法再获取到这个 Monitor 对象,当线程执行完该方法后,它会释放掉这个 Monitor 对象。


1.3 synchronized 静态方法

public static synchronized void method() {
    System.out.println("hello world");
}

字节码:

  public static synchronized void method();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String hello world
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 6: 0
        line 7: 8
}

注意:👆 method 方法的 “args_size = 0”

当一个执行线程去调用静态的同步方法的时候,它发现这个方法本身拥有 ACC_STATIC 和 ACC_SYNCHRONIZED 关键字,那么它(线程)就知道这个方法本身是一个静态的同步方法。那么,执行线程在执行方法的代码块之前,需要先获取到当前方法所在类所对应的Class对象的 Monitor 对象。当方法执行完后,线程会释放掉这个 Monitor 对象。


二,异常的字节码实现

2.1 try-catch 语句块

public void test() {
    try {
        InputStream is = new FileInputStream("test.txt");
        ServerSocket serverSocket = new ServerSocket(9999);
        serverSocket.accept();
    } catch (FileNotFoundException ex) {
    } catch (IOException ex) {
    } catch (Exception ex) {
    } finally {
        System.out.println("finally");
    }
}

字节码:

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #2                  // class java/io/FileInputStream
         3: dup
         4: ldc           #3                  // String test.txt
         6: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
         9: astore_1
        10: new           #5                  // class java/net/ServerSocket
        13: dup
        14: sipush        9999
        17: invokespecial #6                  // Method java/net/ServerSocket."<init>":(I)V
        20: astore_2
        21: aload_2
        22: invokevirtual #7                  // Method java/net/ServerSocket.accept:()Ljava/net/Socket;
        25: pop
        26: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        29: ldc           #9                  // String finally
        31: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        34: goto          84
        37: astore_1
        38: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        41: ldc           #9                  // String finally
        43: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        46: goto          84
        49: astore_1
        50: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        53: ldc           #9                  // String finally
        55: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        58: goto          84
        61: astore_1
        62: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        65: ldc           #9                  // String finally
        67: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        70: goto          84
        73: astore_3
        74: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        77: ldc           #9                  // String finally
        79: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        82: aload_3
        83: athrow
        84: return
      Exception table:
         from    to  target type
             0    26    37   Class java/io/FileNotFoundException
             0    26    49   Class java/io/IOException
             0    26    61   Class java/lang/Exception
             0    26    73   any
      LineNumberTable:
        line 13: 0
        line 14: 10
        line 15: 21
        line 24: 26
        line 25: 34
        line 17: 37
        line 24: 38
        line 25: 46
        line 19: 49
        line 24: 50
        line 25: 58
        line 21: 61
        line 24: 62
        line 25: 70
        line 24: 73
        line 27: 84
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           10      16     1    is   Ljava/io/InputStream;
           21       5     2 serverSocket   Ljava/net/ServerSocket;
            0      85     0  this   Lcom/bayern/shengsiyuan/jvm_lecture/bytecode/lesson13/MyTest3;
      StackMapTable: number_of_entries = 5
        frame_type = 101 /* same_locals_1_stack_item */
          stack = [ class java/io/FileNotFoundException ]
        frame_type = 75 /* same_locals_1_stack_item */
          stack = [ class java/io/IOException ]
        frame_type = 75 /* same_locals_1_stack_item */
          stack = [ class java/lang/Exception ]
        frame_type = 75 /* same_locals_1_stack_item */
          stack = [ class java/lang/Throwable ]
        frame_type = 10 /* same */
}
  • 简化出一个异常流程:
  public void test();
    Code:
      stack=3, locals=4, args_size=1
         0: new           #2                  // class java/io/FileInputStream
         3: dup
         4: ldc           #3                  // String test.txt
         6: invokespecial #4                  // Method java/io/FileInputStream."<init>":(Ljava/lang/String;)V
         
        ......
        37: astore_1                          // 将一个引用赋给了局部变量。这里就是将 FileNotFoundException 对象引用赋给 ex 这个局部变量;可以这么理解「new FileInputStream("test.txt");」如果异常退出,返回的对象就是‘FileNotFoundException’对象,而非‘FileInputStream’对象。因此,此时按顺序,应该是将‘FileNotFoundException’放置到局部变量表索引为 1 的位置(此时,局部变量表索引为 0 的 slot 放置着 this 引用)。如果,「new FileInputStream("test.txt");」正常操作返回,得到的就是‘FileInputStream’对象,这时放入到局部变量表索引 1 的变量就是‘FileInputStream’对象了(即,字节码「9: astore_1」)。
        38: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        41: ldc           #9                  // String finally
        43: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        46: goto          84
        ......        
        84: return
      Exception table:
         from    to  target type
             0    26    37   Class java/io/FileNotFoundException
             0    26    49   Class java/io/IOException
             0    26    61   Class java/lang/Exception
             0    26    73   any
}

「37: astore_1」:将一个引用赋给了局部变量。这里就是将 FileNotFoundException 对象引用赋给 ex 这个局部变量;可以这么理解「new FileInputStream(“test.txt”);」如果异常退出,返回的对象就是‘FileNotFoundException’对象,而非‘FileInputStream’对象。因此,此时按顺序,应该是将‘FileNotFoundException’放置到局部变量表索引为 1 的位置(此时,局部变量表索引为 0 的 slot 放置着 this 引用)。如果,「new FileInputStream(“test.txt”);」正常操作返回,得到的就是‘FileInputStream’对象,这时放入到局部变量表索引 1 的变量就是‘FileInputStream’对象了(即,字节码「9: astore_1」)。


  • any 类型的异常

catch_type 为 0(any)时,表示处理所有的异常

        73: astore_3
        74: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        77: ldc           #9                  // String finally
        79: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        82: aload_3
        83: athrow

👆只有 any 类型的异常,会在执行完「finally{}」代码块后,将异常抛出(执行「athrow」)后直接退出(这里不会再执行「return」字节码指令了,因为这里的异常类型没有被 catch 捕获)。其他类型的异常,都是执行完「catch{}」代码块后,继续往下执行,最后再执行「finally{}」代码块,然后执行退出方法(「return」)。


  • Java 字节码对于异常的处理方式:

    1. 统一采用异常表的方式来对异常进行处理。
    2. 在jdk 1.4.2 之前的版本中,并不是使用异常表的方式来对异常进行处理的,而是采用特定的指令方式。
    3. 当异常处理存在 finally 语句块时,现代化的 JVM 采取的处理方式是将 finally 语句块的字节码拼接到每一个 catch 块后面。换句话说,程序中存在多少个 catch 块,就会在每一个 catch 块后面重复多少个 finally 语句块的字节码。(这种实现方式,能减少跳转,即,goto)


2.2 方法显示声明抛出异常

public void test() throws IOException, FileNotFoundException {
    try {
        InputStream is = new FileInputStream("test.txt");
        ServerSocket serverSocket = new ServerSocket(9999);
        serverSocket.accept();
    } catch (FileNotFoundException ex1) {
    } catch (IOException ex2) {
    } catch (Exception ex3) {
    } finally {
        System.out.println("finally");
    }
}

对应字节码:

public void test() throws java.io.IOException, java.io.FileNotFoundException;
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #2                  // class java/io/FileInputStream
         3: dup
        
        ......
    Exceptions:
      throws java.io.IOException, java.io.FileNotFoundException
}


当方法声明中有throws异常时,字节码会出现’exception attribute’,它和’code attribute’是同级的,都是方法的属性。


三,局部变量表

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放 a)方法参数和b)方法内部定义的局部变量。

局部变量表的容量以变量槽(Variable Slot,下称Slot)为最小单位。

在方法执行时,虚拟机是使用局部变量表完成「参数值」 到 「参数变量列表」的传递过程的,如果执行的是实例方法(非static的方法),那局部变量表中第0位索引的Slot默认是用于传递方法所属对象实例的引用,在方法中可以通过关键字”this”来访问到这个隐含的参数。其余参数则按照参数表顺序排列,占用从1开始的局部变量Slot,参数表分配完毕后,再根据方法体内部定义的变量顺序和作用域分配其余的Slot。

为了尽可能节省栈帧空间,局部变量表中的Slot是可以重用的,方法体中定义的变量,其作用域并不一定会覆盖整个方法体,如果当前字节码PC计数器的值已经超出了某个变量的作用域,那这个变量对应的Slot就可以交给其他变量使用。
不过,这样的设计除了节省栈帧空间以外,还会伴随一些额外的副作用,例如,在某些情况下,Slot的复用会直接影响到系统的垃圾收集行为:

public static void main(String[]args)() {
    byte[] placeholder = new byte[64*1024*1024];
    System.gc();
}

我们在虚拟机运行参数中加上”-verbose:gc”来看看垃圾收集的过程,发现在System.gc()运行后并没有回收这64MB的内存:

没有回收placeholder所占的内存能说得过去,因为在执行System.gc()时,变量placeholder还处于作用域之内,虚拟机自然不敢回收placeholder的内存。那我们把代码修改一下:

# 代码清单 8-2
public static void main(String[]args)() {
    {
        byte[] placeholder = new byte[64*1024*1024];
    }
    System.gc();
}

加入了花括号之后,placeholder的作用域被限制在花括号之内,从代码逻辑上讲,在执行System.gc()的时候,placeholder已经不可能再被访问了,但执行一下这段程序,会发现运行结果如下,还是有64MB的内存没有被回收,这又是为什么呢?

在解释为什么之前,我们先对这段代码进行第二次修改,在调用System.gc()之前加入一行”int a=0;”:

# 代码清单 8-3
public static void main(String[]args)() {
    {
    byte[] placeholder = new byte[64*1024*1024];
    }
    int a=0;
    System.gc();
}

这个修改看起来很莫名其妙,但运行一下程序,却发现这次内存真的被正确回收了。

placeholder能否被回收的根本原因是:局部变量表中的Slot是否还存有关于placeholder数组对象的引用。第一次修改中,代码虽然已经离开了placeholder的作用域,但在此之后,没有任何对局部变量表的读写操作,placeholder原本所占用的Slot还没有被其他变量所复用,所以作为GC Roots一部分的局部变量表仍然保持着对它的关联。
这种关联没有被及时打断,在绝大部分情况下影响都很轻微。但如果遇到一个方法,其后面的代码有一些耗时很长的操作,而前面又定义了占用了大量内存、实际上已经不会再使用的变量,手动将其设置为null值(用来代替那句int a=0,把变量对应的局部变量表Slot清空)便不见得是一个绝对无意义的操作,这种操作可以作为一种在极特殊情形(对象占用内存大、此方法的栈帧长时间不能被回收、方法调用次数达不到JIT的编译条件)下的“奇技”来使用。

但是不应当对赋null值的操作有过多的依赖,更没有必要把它当做一个普遍的编码规则来推广。原因有两点:
① 从编码角度讲,以恰当的变量作用域来控制变量回收时间才是最优雅的解决方法,如代码清单 8-3 那样的场景并不多见。
② 更关键的是,从执行角度讲,使用赋null值的操作来优化内存回收是建立在对字节码执行引擎概念模型的理解之上的。

关于局部变量表,还有一点可能会对实际开发产生影响,就是局部变量不像前面介绍的类变量那样存在“准备阶段”。局部变量就不一样,如果一个局部变量定义了但没有赋初始值是不能使用的。
还好编译器能在编译期间就检查到并提示这一点,即便编译能通过或者手动生成字节码的方式制造出下面代码的效果,字节码校验的时候也会被虚拟机发现而导致类加载失败。


四,<init> 方法

对于字节码文件本身来说, Methods 中是可以不包含 方法的。 同时,就 Java 字节码来说,Methods 中也可以不包含 方法的。

  • abstract 类的字节码的 Methods 中包含 方法,但我们知道抽象类是无法被实例化的,而这里的 方法实例化的是父类 Object 类。Java 中所有的类都会默认继承 Object 类。


  • interface 的字节码的 Methods 中是不包含 方法的!接口是无法被实例化的