java虚拟机(十四)--字节码指令

字节码指令其实是很重要的,在之前学习String等内容,深入到字节码层面很容易找到答案,而不是只是在网上寻找答案,还有可能是错误的。

PS:本文基于jdk1.8

首先写个简单的类:

public class Test {

    public static Integer f1() {
        int a = 1;
        int b = 2;
        return a + b;
    }

    public static void main(String[] args) {
        int m = 100;
        int c = f1();
        System.out.println(m + c);
    }

}

反编译:

先通过javac编译,然后通过javap -verbose Test.class > Test.txt把反编译结果重定向到txt文件中

//类文件
Classfile /D:/Java/project/monitor/target/classes/com/it/test1/Test.class
//最后修改,文件大小
  Last modified 2019-7-16; size 785 bytes
  MD5 checksum 1dc6eb4c2e233f63edbb50e709c20111
//编译来自Test.java
  Compiled from "Test.java"
//以下为类信息
public class com.it.test1.Test
//jdk版本
  minor version: 0
  major version: 52
//类的访问修饰符,public和super
  flags: ACC_PUBLIC, ACC_SUPER
//2、常量池,下面1,2,3,4....50,相当于索引,这部分简单理解就行了,主要是程序部分
Constant pool:
//Methodref方法引用,#8.#29代表引用第8行和29行
   #1 = Methodref          #8.#29         // java/lang/Object."<init>":()V
//自动装箱,执行Integer.valueOf()
   #2 = Methodref          #30.#31        // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
   #3 = Methodref          #7.#32         // com/it/test1/Test.f1:()Ljava/lang/Integer;
   #4 = Methodref          #30.#33        // java/lang/Integer.intValue:()I
//Fieldref字段引用,L为引用类型,格式为L ClassName;注意最后还有冒号;
   #5 = Fieldref           #34.#35        // java/lang/System.out:Ljava/io/PrintStream;
   #6 = Methodref          #36.#37        // java/io/PrintStream.println:(I)V
//
   #7 = Class              #38            // com/it/test1/Test
   #8 = Class              #39            // java/lang/Object
#Utf8可以理解为字符串,<init>相当于构造函数
   #9 = Utf8               <init>
//()V,无参,返回值为void
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable    //本地变量表
  #14 = Utf8               this
  #15 = Utf8               Lcom/it/test1/Test;
  #16 = Utf8               f1
  #17 = Utf8               ()Ljava/lang/Integer;
  #18 = Utf8               a
  #19 = Utf8               I
  #20 = Utf8               b
  #21 = Utf8               main
  #22 = Utf8               ([Ljava/lang/String;)V
  #23 = Utf8               args
  #24 = Utf8               [Ljava/lang/String;
  #25 = Utf8               m
  #26 = Utf8               c
  #27 = Utf8               SourceFile
  #28 = Utf8               Test.java
//NameAndType,名称和返回值
  #29 = NameAndType        #9:#10         // "<init>":()V
  #30 = Class              #40            // java/lang/Integer
  #31 = NameAndType        #41:#42        // valueOf:(I)Ljava/lang/Integer;
  #32 = NameAndType        #16:#17        // f1:()Ljava/lang/Integer;
  #33 = NameAndType        #43:#44        // intValue:()I
  #34 = Class              #45            // java/lang/System
  #35 = NameAndType        #46:#47        // out:Ljava/io/PrintStream;
  #36 = Class              #48            // java/io/PrintStream
  #37 = NameAndType        #49:#50        // println:(I)V
  #38 = Utf8               com/it/test1/Test
  #39 = Utf8               java/lang/Object
  #40 = Utf8               java/lang/Integer
  #41 = Utf8               valueOf
  #42 = Utf8               (I)Ljava/lang/Integer;
  #43 = Utf8               intValue
  #44 = Utf8               ()I
  #45 = Utf8               java/lang/System
  #46 = Utf8               out
  #47 = Utf8               Ljava/io/PrintStream;
  #48 = Utf8               java/io/PrintStream
  #49 = Utf8               println
  #50 = Utf8               (I)V
//程序部分开始
{
  public com.it.test1.Test();
//默认构造器,无参,无返回值
    descriptor: ()V
//修饰符public
    flags: ACC_PUBLIC
//Code部分
    Code:
# 操作数栈的深度2
# 本地变量表最大长度(slot为单位),64位的是2个,其他是1个,索引从0开始,如果是非static方法索引0代表this,后面是入参,后面是本地变量
# 1个参数,实例方法多一个this参数
//args_size
      stack=1, locals=1, args_size=1
//aload_<n>从本地变量加载引用,n为当前栈帧中局部变量数组的索引
         0: aload_0
//invokespecial调用实例方法; 对超类,私有和实例初始化方法调用的特殊处理
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
//行号的表,line后面的数字代表代码的行号,代表上面字节码中的0,就是aload_0
      LineNumberTable:
        line 3: 0
//本地变量表,非static方法,0位this,static方法,就是第一个变量
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/it/test1/Test;

  public static java.lang.Integer f1();
    descriptor: ()Ljava/lang/Integer;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
//将常量1push到操作数栈中
         0: iconst_1
//将操作数栈中栈顶元素存储到本地变量表的索引0中
//这两步对应着int a = 1;
         1: istore_0
         2: iconst_2
//这两步对应着int b = 2;
         3: istore_1
//将int类型的本地变量0的数据压入操作数栈
         4: iload_0
         5: iload_1
//int类型相加
         6: iadd
//调用了第二行,是一个方法引用,执行完毕,清空操作数栈,此时本地变量表数据还在
         7: invokestatic  #2                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
//返回引用,会把本地变量表清空
        10: areturn
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            2       9     0     a   I
            4       7     1     b   I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: bipush        100
         2: istore_1
//invokestatic执行静态方法,invokevirtual执行实例方法
         3: invokestatic  #3                  // Method f1:()Ljava/lang/Integer;
         6: invokevirtual #4                  // Method java/lang/Integer.intValue:()I
         9: istore_2
        10: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
        13: iload_1
        14: iload_2
        15: iadd
        16: invokevirtual #6                  // Method java/io/PrintStream.println:(I)V
        19: return
      LineNumberTable:
        line 12: 0
        line 13: 3
        line 14: 10
        line 15: 19
      LocalVariableTable://数组类型参数,作为本地变量表索引0位置的数据
        Start  Length  Slot  Name   Signature
            0      20     0  args   [Ljava/lang/String;
            3      17     1     m   I
           10      10     2     c   I
}
SourceFile: "Test.java"

上面对基本的字节码都有解释了,这里以f1()为例,通过图例更加详细的解释

字节码相关内容,可以参考官方文档:https://docs.oracle.com/javase/specs/jvms/se8/html/

1、flags表示类访问和属性修饰符

字段描述符:

方法描述符:

i++和++i

代码:

public static void f1() {
    int i = 0;
    int j = i++;
}

public static void f2() {
    int i = 0;
    int j = ++i;
}

public static void main(String[] args) {
    f1();
    f2();
}

反编译:

f1():
0: iconst_0    //常量0push到操作数栈的栈顶
1: istore_0    //将操作数栈的栈顶数据存储到本地变量表的索引0的位置
2: iload_0    
3: iinc          0, 1    //将本地变量表的索引0的数据+1
6: istore_1    //将操作数栈的栈顶数据存储到本地变量表的索引1的位置
7: return    

f2():
0: iconst_0
1: istore_0
2: iinc          0, 1    //将本地变量表的索引0的数据+1
5: iload_0    //此时索引0的数据为1,load到操作数栈
6: istore_1    ////将操作数栈的栈顶数据存储到本地变量表的索引1的位置
7: return

从上面我们很容易看到二者的区别

PS:for循环中i++和++i没有效率差别,字节码完全一样的

try-Cache字节码:

代码:

public static String f1() {
    String str = "hello1";
    try{
        return str;
    }
    finally{
        str = "imooc";
    }
}

public static void main(String[] args) {
    f1();
}

反编译:

0: ldc           #2                  //从运行时常量池中加载字符串hello,然后push到操作数栈
2: astore_0    //将操作数栈的栈顶数据存储到本地变量表的索引0的位置
3: aload_0    //
4: astore_1    //字符串hello,存在本地变量表的两个位置
5: ldc           #3                  // String imooc
7: astore_0    //将字符串imooc存储到本地变量表的索引0的位置,用imooc覆盖了hello
8: aload_1    //load本地变量表中索引1位置的数据
9: areturn    //所以返回的是hello,而不是imooc

//假如发生异常,就会走下面的代码
10: astore_2    //将异常存储到本地变量表的索引2的位置
11: ldc           #3                  // String imooc
13: astore_0
14: aload_2    //将索引2的位置的异常信息load出去
15: athrow    //跑出异常

我们从字节码中看到无论是否发生异常,都会执行finally的内容,但是我们的return并不是finally的内容

我们再测试一下:

public static String f1() {
    String str = "hello1";
    try{
        return str;
    }
    finally{
        str = "imooc";
        return "111";
    }
}

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

反编译:

0: ldc           #2                  // String hello1
2: astore_0
3: aload_0
4: astore_1
5: ldc           #3                  // String imooc
7: astore_0
8: ldc           #4                  // String 111    将字符串111push到操作数栈的栈顶
10: areturn

11: astore_2
12: ldc           #3                  // String imooc
14: astore_0
15: ldc           #4                  // String 111    将字符串111push到操作数栈的栈顶
17: areturn

所以无论是否发生异常,返回的都是字符串111,我们一般情况下不要在finally中使用return,很容易出现错误

String Constant Variable:

我们知道String的字符串拼接就是会new一个StringBuilder,然后append这个字符串,然后调用toString(),在for循环中效率很低。但是如果是final修饰的常量就不一定了。

代码:

public static void f1() {
    final String x="hello";
    final String y=x+"world";
    String z=x+y;
    System.out.println(z);
}

public void f2(){
    final String x="hello";
    String y=x+"world";
    String z=x+y;
    System.out.println(z);
}

反编译:

f1():
0: ldc           #2                  // String hello    //从常量池把hello字符串push到操作数栈
 2: astore_0
 3: ldc           #3                  // String helloworld    //从常量池把helloworld字符串push到操作数栈
 5: astore_1
 6: ldc           #4                  // String hellohelloworld    //从常量池把hellohelloworld字符串push到操作数栈
 8: astore_2
 9: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
12: aload_2
13: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: return

f2():
 0: ldc           #2                  // String hello
 2: astore_1
 3: ldc           #3                  // String helloworld
 5: astore_2
 6: new           #7                  // class java/lang/StringBuilder    //new StringBuilder
 9: dup        //复制操作数栈的栈顶值
10: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V    //调用无参构造器初始化
13: ldc           #2                  // String hello    
15: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
18: aload_2
19: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
22: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
25: astore_3
26: getstatic     #5                  // Field java/lang/System.out:Ljava/io/PrintStream;
29: aload_3
30: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
33: return

我们可以看到对于final修饰String引用,在编译器就进行了优化,x+"world"直接优化成"helloworld"

原文地址:https://www.cnblogs.com/huigelaile/p/11195642.html