java字节码基础篇

52 阅读20分钟

Java中的字节码,英文名为bytecode, 是Java代码编译后的中间代码格式。JVM需要读取并解析字节码才能执行相应的任务。

Java字节码是JVM的指令集。JVM加载字节码格式的class文件,校验之后通过JIT编译器转换为本地机器代码执行。

现在市面上通过修改字节码来实现某个功能的技术框架屡见不鲜,例如Mock,AOP,Profile等工具框架,都是基于字节码操作来实现的,所以简单了解一下Java字节码技术还是有必要的。

下面将通过介绍Java语言中的一些常见特性,来看一下字节码的应用,由于Java特性非常多,这里我们仅介绍一些经常遇到的特性。

Java字节码简介

Java字节码由单字节(byte)的指令组成,理论上最多支持256个操作码(opcode)。实际上Java只使用了200左右的操作码。

字节码指令可以大致分为5类:

  • 栈操作指令,包括与局部变量交互的指令
  • 程序流程控制指令
  • 对象操作指令,包括方法调用指令
  • 算术运算以及类型转换指令
  • 特殊指令,比如同步(synchronization)指令,异常处理指令等

我们先来看一段简单的案例。下面是Java源代码

public class Demo {

  private String s;

  public Demo() {
    this.s = "hello world";
  }

  public Demo(String s) {
    this.s = s;
  }

  public void foo() {
    System.out.println(s);
  }

  public static void main(String[] args) {
    Demo demo = new Demo("shawn");
    demo.foo();
  }
}

通过javac编译后生成Demo.class文件,在通过javap -c -v Demo.class指令查看操作码,结果如下:

Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
Classfile /E:/workspace/myself-projects/learning-notes/programming_languages/java-learning/deep-in-java/jvm/target/classes/com/shawn/study/deep/in/java/jvm/Demo.class
  Last modified 2023-11-3; size 898 bytes
  // MD5 CheckSum
  MD5 checksum c9e1605d3b79bb7b1d0c9b51f4951ef5
  Compiled from "Demo.java"
public class com.shawn.study.deep.in.java.jvm.Demo
  // JDK version
  minor version: 0
  major version: 55
  flags: ACC_PUBLIC, ACC_SUPER // 访问标识符
// 常量池
Constant pool:
   #1 = Methodref          #10.#30        // java/lang/Object."<init>":()V
   #2 = String             #31            // hello world
   #3 = Fieldref           #6.#32         // com/shawn/study/deep/in/java/jvm/Demo.s:Ljava/lang/String;
   #4 = Fieldref           #33.#34        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #35.#36        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = Class              #37            // com/shawn/study/deep/in/java/jvm/Demo
   #7 = String             #38            // shawn
   #8 = Methodref          #6.#39         // com/shawn/study/deep/in/java/jvm/Demo."<init>":(Ljava/lang/String;)V
   #9 = Methodref          #6.#40         // com/shawn/study/deep/in/java/jvm/Demo.foo:()V
  #10 = Class              #41            // java/lang/Object
  #11 = Utf8               s
  #12 = Utf8               Ljava/lang/String;
  #13 = Utf8               <init>
  #14 = Utf8               ()V
  #15 = Utf8               Code
  #16 = Utf8               LineNumberTable
  #17 = Utf8               LocalVariableTable
  #18 = Utf8               this
  #19 = Utf8               Lcom/shawn/study/deep/in/java/jvm/Demo;
  #20 = Utf8               (Ljava/lang/String;)V
  #21 = Utf8               MethodParameters
  #22 = Utf8               foo
  #23 = Utf8               main
  #24 = Utf8               ([Ljava/lang/String;)V
  #25 = Utf8               args
  #26 = Utf8               [Ljava/lang/String;
  #27 = Utf8               demo
  #28 = Utf8               SourceFile
  #29 = Utf8               Demo.java
  #30 = NameAndType        #13:#14        // "<init>":()V
  #31 = Utf8               hello world
  #32 = NameAndType        #11:#12        // s:Ljava/lang/String;
  #33 = Class              #42            // java/lang/System
  #34 = NameAndType        #43:#44        // out:Ljava/io/PrintStream;
  #35 = Class              #45            // java/io/PrintStream
  #36 = NameAndType        #46:#20        // println:(Ljava/lang/String;)V
  #37 = Utf8               com/shawn/study/deep/in/java/jvm/Demo
  #38 = Utf8               shawn
  #39 = NameAndType        #13:#20        // "<init>":(Ljava/lang/String;)V
  #40 = NameAndType        #22:#14        // foo:()V
  #41 = Utf8               java/lang/Object
  #42 = Utf8               java/lang/System
  #43 = Utf8               out
  #44 = Utf8               Ljava/io/PrintStream;
  #45 = Utf8               java/io/PrintStream
  #46 = Utf8               println
{
  public com.shawn.study.deep.in.java.jvm.Demo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: ldc           #2                  // String hello world
         7: putfield      #3                  // Field s:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 7: 0
        line 8: 4
        line 9: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/shawn/study/deep/in/java/jvm/Demo;

  public com.shawn.study.deep.in.java.jvm.Demo(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #3                  // Field s:Ljava/lang/String;
         9: return
      LineNumberTable:
        line 11: 0
        line 12: 4
        line 13: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/shawn/study/deep/in/java/jvm/Demo;
            0      10     1     s   Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      s

  public void foo();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: aload_0
         4: getfield      #3                  // Field s:Ljava/lang/String;
         7: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: return
      LineNumberTable:
        line 16: 0
        line 17: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/shawn/study/deep/in/java/jvm/Demo;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: new           #6                  // class com/shawn/study/deep/in/java/jvm/Demo
         3: dup
         4: ldc           #7                  // String shawn
         6: invokespecial #8                  // Method "<init>":(Ljava/lang/String;)V
         9: astore_1
        10: aload_1
        11: invokevirtual #9                  // Method foo:()V
        14: return
      LineNumberTable:
        line 20: 0
        line 21: 10
        line 22: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  args   [Ljava/lang/String;
           10       5     1  demo   Lcom/shawn/study/deep/in/java/jvm/Demo;
    MethodParameters:
      Name                           Flags
      args
}
SourceFile: "Demo.java"

javap -c -v能够看到更多的信息,比如常量池,本地变量表,MD5 Check等。

解读Java字节码

我们以其中一个代码片段为例,讲解一下其中的属性

public com.shawn.study.deep.in.java.jvm.Demo(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aload_1
         6: putfield      #3                  // Field s:Ljava/lang/String;
         9: return
      LineNumberTable:
        line 11: 0
        line 12: 4
        line 13: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/shawn/study/deep/in/java/jvm/Demo;
            0      10     1     s   Ljava/lang/String;
    MethodParameters:
      Name                           Flags
      s
  • descriptor: 表示方法的描述。

    • 其中小括号内是入参信息/形参信息;
    • L表示对象;
    • 后面的java/lang/String就是类名称;
    • 小括号后面的 V 则表示这个方法的返回值是 void;
  • flags: 表示方法的访问标志,ACC_PUBLIC表示构造方法的访问标志为public。

  • Code: 是方法表,表示Java方法经过编译后的字节码指令,就是以字节码的形式表达Java方法的执行过程。

    • stack:表示操作数栈的大小
    • locals:表示局部变量表的大小
    • args_size:表示方法形参的数量,但从源码来看这个构造方法的参数就1个啊,为什么args_size为2呢?实际上实例方法和构造方法除了方法形参的数量,还得加上对象实例的数量,也就是this。但静态方法是没有this引用的,所以计算args_size的时候无需+1,案例可以查看main方法的字节码指令
    • LineNumberTable:表示java源代码(.java文件)的行号和字节码(.class文件)的行号之间的对应关系,主要是方便在异常发生的时候,在堆栈中显示出源代码出错的行号;以及在调试过程中,按照源代码的行号设置断点。
    • LocalVariableTable:用来描述栈帧的局部变量表中变量与java源码中变量的对应关系。
  • MethodParameters:方法形参

Code表示方法表,记录了一堆字节码指令,如上所示:

  • aload_0:用于加载局部方法表中的参数到操作数栈中。后面的0,表示局部变量表的Slot的位点。后面还有aload_1,就是用于加载局部变量s的。
  • invokespecial:我们知道调用构造函数,会优先调用父类的构造函数,但这不是 JVM 自动执行的, 而是由程序指令控制,这个指令就是invokespecial。
  • putfield:将值赋给实例字段

我们也能看到invokespecial #1等指令后带着#,那这个又是代表什么意思呢?事实上,#表示对常量池的引用

Constant pool:
   #1 = Methodref          #10.#30        // java/lang/Object."<init>":()V
   #2 = String             #31            // hello world
   #3 = Fieldref           #6.#32         // com/shawn/study/deep/in/java/jvm/Demo.s:Ljava/lang/String;
   #4 = Fieldref           #33.#34        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = Methodref          #35.#36        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #6 = Class              #37            // com/shawn/study/deep/in/java/jvm/Demo
   #7 = String             #38            // shawn
  • invokespecial #1: 引用的就是常量池里的#1 = Methodref #10.#30
  • putfield #3:引用的就是常量池里的#3 = Fieldref #6.#32

常量池中的常量定义解读如下:以#1 = Methodref #10.#30为例

  • #1:常量编号, 该文件中其他地方可以引用。
  • =:等号就是分隔符。
  • Methodref:表明这个常量指向的是一个方法;具体是哪个类的哪个方法呢? 类指向的#10, 方法签名指向的 #30; 当然双斜线注释后面已经解析出来可读性比较好的说明了。

常量池就是一个常量的大字典,使用编号的方式把程序里用到的各类常量统一管理起来,这样在字节码操作里,只需要引用编号即可。

指令我们知道怎么解读,指令前面的数字是什么意思呢?0: aload_0前面的0表示啥呢?

实际上就是.java中的方法源代码编译后让JVM真正执行的操作码(有一部分操作码会附带有操作数, 也会占用字节码数组中的空间)。为了帮助人们理解,反编译后看到的是十六进制操作码所对应的助记符,十六进制值操作码与助记符的对应关系,以及每一个操作码的用处可以查看Oracle官方文档进行了解,在需要用到时进行查阅即可,例如下面的 aload_n, invokespecial, putfield, return操作指令

aload_n会占用一个槽位,对应的字节码分别是

  • aload_0 = 42 (0x2a)
  • aload_1 = 43 (0x2b)
  • aload_2 = 44 (0x2c)
  • aload_3 = 45 (0x2d)

invokespecial会占用三个槽位:1个用于存放操作码指令自身,其余两个用于存放操作数。对应的格式如下:invokespecial indexbyte1 indexbyte2 对应的16进制指令是:invokespecial = 183 (0xb7)

putfield也会占用三个槽位,格式如下:putfield indexbyte1 indexbyte2,对应的16进制指令是:putfield = 181 (0xb5)

return占用一个槽位,对应的16进制指令是:return = 177 (0xb1)

常见的指令集

操作数栈

想要深入了解字节码技术,我们需要先对字节码的执行模型有所了解。

每个线程都有一个独立线程栈(JVM stack),用于存储栈帧(Frame)。每一次方法调用,JVM都会自动创建一个栈帧。栈帧由操作数栈, 局部变量数组以及一个class引用组成。class引用指向当前方法在运行时常量池中对应的 class。

其中操作数栈,来存放计算的操作数以及返回结果。执行每一条指令之前,Java虚拟机要求该指令的操作数已被压入操作数栈中。在执行指令时,Java 虚拟机会将该指令所需的操作数弹出,并且将指令的结果重新压入栈中。

过程符号
变量到操作数栈iload, iload_, lload, lload_, fload, fload_, dload, dload_, aload, aload_
操作数栈到变量istore, istore_, lstore, lstore_, fstore, fstore_, dstore, dstor_, astore, astore_
常数到操作数栈bipush, sipush, ldc, ldc_w, ldc2_w, aconst_null, iconst_ml, iconst_, lconst_, fconst_, dconst_
把数据装载到操作数栈baload, caload, saload, iaload, laload, faload, daload, aaload
从操作数栈存存储到数组bastore, castore, sastore, iastore, lastore, fastore, dastore, aastore
操作数栈管理pop, pop2, dup, dup2, dup_xl, dup2_xl, dup_x2, dup2_x2, swap

算术指令集

Java 字节码中有许多指令可以执行算术运算。实际上,指令集中有很大一部分表示都是关于数学运算的。还有一部分用于类型转换

public class com.shawn.study.deep.in.java.jvm.AlgorithmDemo {
  public com.shawn.study.deep.in.java.jvm.AlgorithmDemo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int add(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: ireturn

  public double div(int, int);
    Code:
       0: iload_1
       1: i2d
       2: iload_2
       3: i2d
       4: ddiv
       5: dreturn

  public int multi(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: imul
       3: ireturn

  public int sub(int, int);
    Code:
       0: iload_1
       1: iload_2
       2: isub
       3: ireturn

  public byte add(byte, byte);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: i2b
       4: ireturn

  public short add(short, short);
    Code:
       0: iload_1
       1: iload_2
       2: iadd
       3: i2s
       4: ireturn

  public java.lang.String concat(char, char);
    Code:
       0: iload_1
       1: invokestatic  #2                  // Method java/lang/String.valueOf:(C)Ljava/lang/String;
       4: iload_2
       5: invokestatic  #2                  // Method java/lang/String.valueOf:(C)Ljava/lang/String;
       8: invokevirtual #3                  // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
      11: areturn

  public boolean merge(boolean, boolean);
    Code:
       0: iload_1
       1: ifeq          12
       4: iload_2
       5: ifeq          12
       8: iconst_1
       9: goto          13
      12: iconst_0
      13: ireturn

  public float add(float, float);
    Code:
       0: fload_1
       1: fload_2
       2: fadd
       3: freturn

  public double add(double, double);
    Code:
       0: dload_1
       1: dload_3
       2: dadd
       3: dreturn

  public long add(long, long);
    Code:
       0: lload_1
       1: lload_3
       2: ladd
       3: lreturn
}

算术操作码和类型

对于所有数值类型(int, long, double, float),都有加,减,乘,除,取反的指令。对于byte和char, boolean,JVM是当做int来处理的

add(+)sub(-)multi(*)divide(/)remainder(%)negate(-())
booleaniaddisubimulidiviremineg
byteiaddisubimulidiviremineg
shortiaddisubimulidiviremineg
intiaddisubimulidiviremineg
chariaddisubimulidiviremineg
longladdlsublmulldivlremlneg
floatfaddfsubfmulfdivfremfneg
doubledadddsubdmulddivdremdneg

类型转换操作码

byteshortintlongfloatdoublechar
inti2bi2s-i2li2fi2di2c
long--l2i-l2fl2d-
float--f2if2l-f2d-
double--d2id2ld2f--

按位运算符

public class BitAlgo {

  public void testByte() {
    byte a = 1;
    byte b = 2;
    int c = a << 2;
    int d = a >> 2;
    int e = b >>> 2;
    int f = a | b;
    int g = a & b;
    int h = a ^ b;
  }

  public void testShort() {
    short a = 1;
    short b = 2;
    int c = a << 2;
    int d = a >> 2;
    int e = b >>> 2;
    int f = a | b;
    int g = a & b;
    int h = a ^ b;
  }

  public void testChar() {
    char a = 1;
    char b = 2;
    int c = a << 2;
    int d = a >> 2;
    int e = b >>> 2;
    int f = a | b;
    int g = a & b;
    int h = a ^ b;
  }

  public void testInt() {
    int a = 1;
    int b = 2;
    int c = a << 2;
    int d = a >> 2;
    int e = b >>> 2;
    int f = a | b;
    int g = a & b;
    int h = a ^ b;
  }

  public void testLong() {
    long a = 1l;
    long b = 2l;
    long c = a << 2;
    long d = a >> 2;
    long e = b >>> 2;
    long f = a | b;
    long g = a & b;
    long h = a ^ b;
  }
}

编译后:

public class com.shawn.study.deep.in.java.jvm.BitAlgo {
  public com.shawn.study.deep.in.java.jvm.BitAlgo();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public void testByte();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iconst_2
       6: ishl
       7: istore_3
       8: iload_1
       9: iconst_2
      10: ishr
      11: istore        4
      13: iload_2
      14: iconst_2
      15: iushr
      16: istore        5
      18: iload_1
      19: iload_2
      20: ior
      21: istore        6
      23: iload_1
      24: iload_2
      25: iand
      26: istore        7
      28: iload_1
      29: iload_2
      30: ixor
      31: istore        8
      33: return

  public void testShort();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iconst_2
       6: ishl
       7: istore_3
       8: iload_1
       9: iconst_2
      10: ishr
      11: istore        4
      13: iload_2
      14: iconst_2
      15: iushr
      16: istore        5
      18: iload_1
      19: iload_2
      20: ior
      21: istore        6
      23: iload_1
      24: iload_2
      25: iand
      26: istore        7
      28: iload_1
      29: iload_2
      30: ixor
      31: istore        8
      33: return

  public void testChar();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iconst_2
       6: ishl
       7: istore_3
       8: iload_1
       9: iconst_2
      10: ishr
      11: istore        4
      13: iload_2
      14: iconst_2
      15: iushr
      16: istore        5
      18: iload_1
      19: iload_2
      20: ior
      21: istore        6
      23: iload_1
      24: iload_2
      25: iand
      26: istore        7
      28: iload_1
      29: iload_2
      30: ixor
      31: istore        8
      33: return

  public void testInt();
    Code:
       0: iconst_1
       1: istore_1
       2: iconst_2
       3: istore_2
       4: iload_1
       5: iconst_2
       6: ishl
       7: istore_3
       8: iload_1
       9: iconst_2
      10: ishr
      11: istore        4
      13: iload_2
      14: iconst_2
      15: iushr
      16: istore        5
      18: iload_1
      19: iload_2
      20: ior
      21: istore        6
      23: iload_1
      24: iload_2
      25: iand
      26: istore        7
      28: iload_1
      29: iload_2
      30: ixor
      31: istore        8
      33: return

  public void testLong();
    Code:
       0: lconst_1
       1: lstore_1
       2: ldc2_w        #2                  // long 2l
       5: lstore_3
       6: lload_1
       7: iconst_2
       8: lshl
       9: lstore        5
      11: lload_1
      12: iconst_2
      13: lshr
      14: lstore        7
      16: lload_3
      17: iconst_2
      18: lushr
      19: lstore        9
      21: lload_1
      22: lload_3
      23: lor
      24: lstore        11
      26: lload_1
      27: lload_3
      28: land
      29: lstore        13
      31: lload_1
      32: lload_3
      33: lxor
      34: lstore        15
      36: return
}
byteshortintlongfloatdoublechar
移位ishl/ishr/iushrishl/ishr/iushrishl/ishr/iushrlshl/lshr/lushr--ishl/ishr/iushr
按位或iorioriorlor--ior
按位与iandiandiandland--iand
按位异或ixorixorixorlxor--ixor

流程指令集

源代码

public class ControlTests {

  public void forLoop() {
    int a = 49;
    for (int i = 0; i < 100; i++) {
      if (i == a) {
        continue;
      } else if (i == 56) {
        break;
      }
    }
  }

  public void forEachLoop() {
    int[] arr = new int[10];
    Arrays.fill(arr, 1);
    for (int i : arr) {
      System.out.println(i);
    }
  }

  public void whileLoop() {
    int[] arr = new int[10];
    Arrays.fill(arr, 1);
    int i = 0;
    while (i < arr.length) {
      System.out.println(arr[i]);
      i++;
    }
  }

  public void doWhileLoop() {
    int[] arr = new int[10];
    Arrays.fill(arr, 1);
    int i = 0;
    do {
      System.out.println(arr[i]);
      i++;
    } while (i < arr.length);
  }

  public void switchCase() {
    int i = new Random().nextInt(2);
    switch (i) {
      case 0:
        System.out.println(0);
        break;
      case 1:
        System.out.println(1);
        break;
      default:
        System.out.println("default");
        break;
    }
  }
}

编译后(只截取了forLoop方法)

public void forLoop();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: bipush        49
         2: istore_1
         3: iconst_0
         4: istore_2
         5: iload_2
         6: bipush        100
         8: if_icmpge     34
        11: iload_2
        12: iload_1
        13: if_icmpne     19
        16: goto          28
        19: iload_2
        20: bipush        56
        22: if_icmpne     28
        25: goto          34
        28: iinc          2, 1
        31: goto          5
        34: return
      LineNumberTable:
        line 9: 0
        line 10: 3
        line 11: 11
        line 12: 16
        line 13: 19
        line 14: 25
        line 10: 28
        line 17: 34
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            5      29     2     i   I
            0      35     0  this   Lcom/shawn/study/deep/in/java/jvm/ControlTests;
            3      32     1     a   I

我们只分析forLoop方法。

根据LineNumberTable可知编号【3~28】用于循环控制

【8: if_icmpge 34 解读: if, integer, compare, great equal】, 如果一个数的值大于或等于另一个值,则程序执行流程跳转到pc=34的地方继续执行。

【goto】指的是跳转到哪里执行,值得一提的是,java还保留了goto关键字,但是java源代码中不会使用到。

【iinc】局部变量增加常量,是for循环中用于递增的循环计数器,根据【iinc 2, 1】可知,局部变量i加1后,然后在赋值给i。

  • 条件分支:if、ifnull、ifnonnull、if_icmp等
  • 复合分支:tableswitch、lookupswitch
  • 无条件分支:goto、goto_w、jsr、jsr_w、ret

对象操作方法调用指令集

创建对象

我们都知道 new是 Java 编程语言中的一个关键字, 但其实在字节码中,也有一个指令叫做 new。 当我们创建类的实例时, 编译器会生成类似下面这样的操作码:

0: new #2 // class demo/jvm0104/HelloByteCode
3: dup
4: invokespecial #3 // Method "<init>":()V

当你同时看到 new, dup 和 invokespecial 指令在一起时,那么一定是在创建类的实例对象!

为什么是三条指令而不是一条呢?这是因为:

  • new 指令只是创建对象,但没有调用构造函数。
  • invokespecial 指令用来调用某些特殊方法的, 当然这里调用的是构造函数。
  • dup 指令用于复制栈顶的值。

由于构造函数调用不会返回值,所以如果没有 dup 指令, 在对象上调用方法并初始化之后,操作数栈就会是空的,在初始化之后就会出问题, 接下来的代码就无法对其进行处理。

这就是为什么要事先复制引用的原因,为的是在构造函数返回之后,可以将对象实例赋值给局部变量或某个字段。因此,接下来的那条指令一般是以下几种:

  • astore {N} or astore_{N} – 赋值给局部变量,其中 {N} 是局部变量表中的位置。
  • putfield – 将值赋给实例字段
  • putstatic – 将值赋给静态字段

在调用构造函数的时候,其实还会执行另一个类似的方法 ,甚至在执行构造函数之前就执行了。

还有一个可能执行的方法是该类的静态初始化方法 , 但 并不能被直接调用,而是由这些指令触发的: new, getstatic, putstatic or invokestatic。

也就是说,如果创建某个类的新实例, 访问静态字段或者调用静态方法,就会触发该类的静态初始化方法【如果尚未初始化】。

实际上,还有一些情况会触发静态初始化, 详情请参考 JVM 规范: [docs.oracle.com/javase/spec…

方法调用

方法调用含义
invokevirtual用于调用公共,受保护和打包私有方法。
invokestaticstatic方法
invokeinterface运行时搜索合适方法调用
invokespecial包括实例初始化方法、父类方法
invokedynamic运行时动态解析出调用点限定符所引用方法

方法返回

方法返回含义
ireturn当前方法返回int
lreturn当前方法返回long
freturn当前方法返回float
dreturn当前方法返回double
areturn当前方法返回ref

异常处理指令集

public void execute() {
    int i = 0;
    try {
      i = 100 / 0;
    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      i = 1;
    }
  }

编译后

public void execute();
    Code:
      stack=3, locals=4, args_size=1
         0: iconst_0
         1: istore_1
         2: bipush        100
         4: iconst_0
         5: idiv
         6: istore_1
         7: iconst_1
         8: istore_1
         9: goto          27
        12: astore_2
        13: new           #3                  // class java/lang/RuntimeException
        16: dup
        17: aload_2
        18: invokespecial #4                  // Method java/lang/RuntimeException."<init>":(Ljava/lang/Throwable;)V
        21: athrow
        22: astore_3
        23: iconst_1
        24: istore_1
        25: aload_3
        26: athrow
        27: return
      Exception table:
         from    to  target type
             2     7    12   Class java/lang/Exception
             2     7    22   any
            12    23    22   any
      LineNumberTable:
        line 6: 0
        line 8: 2
        line 12: 7
        line 13: 9
        line 9: 12
        line 10: 13
        line 12: 22
        line 13: 25
        line 14: 27
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           13       9     2     e   Ljava/lang/Exception;
            0      28     0  this   Lcom/shawn/study/deep/in/java/jvm/ExceptionDemo;
            2      26     1     i   I

Java程序显式抛出异常: athrow指令。在Java虚拟机中,处理异常(catch语句)不是由字节码指令来实现,而是采用带有一个叫 Exception table 的异常表来完成的:

  • from 指定字节码索引的开始位置
  • to 指定字节码索引的结束位置
  • target 异常处理的起始位置
  • type 异常类型

也就是说,只要在 from 和 to 之间发生了异常,就会跳转到 target 所指定的位置。当type为any时,表示无论如何都需要跳转到target位置继续执行。

语法糖

装箱拆箱

Integer a = 1000;
int b = a * 10;
return b;

编译后

0: sipush        1000
3: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
6: astore_1
7: aload_1
8: invokevirtual #3                  // Method java/lang/Integer.intValue:()I
11: bipush        10
13: imul
14: istore_2
15: iload_2
16: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
19: areturn

通过观察字节码,我们发现赋值操作使用的是 Integer.valueOf 方法,在进行乘法运算的时候,调用了 Integer.intValue 方法来获取基本类型的值。在方法返回的时候,再次使用了 Integer.valueOf 方法对结果进行了包装。

这就是 Java 中的自动装箱拆箱的底层实现。

注解

public @interface MyAnnotation {
}

@MyAnnotation
public class AnnotationDemo {
    @MyAnnotation
    public void test(@MyAnnotation  int a){

    }
}

编译后

{
  public AnnotationDemo();
    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 2: 0

  public void test(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=2, args_size=2
         0: return
      LineNumberTable:
        line 6: 0
    RuntimeInvisibleAnnotations:
      0: #11()
    RuntimeInvisibleParameterAnnotations:
      0:
        0: #11()
}
SourceFile: "AnnotationDemo.java"
RuntimeInvisibleAnnotations:
  0: #11()

可以看到,无论是类的注解,还是方法注解,都是由一个叫做 RuntimeInvisibleAnnotations 的结构来存储的,而参数的存储,是由 RuntimeInvisibleParameterAnotations 来保证的。

参考文献

The Java® Virtual Machine Specification

基本功 | Java即时编译器原理解析及实践

字节码增强技术探索