zoukankan      html  css  js  c++  java
  • 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"

  • 相关阅读:
    火眼金睛算法,教你海量短文本场景下去重
    CynosDB技术详解——架构设计
    CynosDB技术详解——存储集群管理
    解决 "Script Error" 的另类思路
    Go 语言实践(一)
    Vue.js的复用组件开发流程
    MYSQL中的COLLATE是什么?
    Blending
    AlphaTesting
    Culling & Depth Testing
  • 原文地址:https://www.cnblogs.com/huigelaile/p/11195642.html
Copyright © 2011-2022 走看看