Java字节码指令探索

237 阅读10分钟

先写一段java代码

public class Hello {
    public Hello() {
    }

    public static void main(String[] args) {
        int a = 1;
        int b = 2;
        int c = add(a, b);
        String d = "abc";
        System.out.println(c + d);

        for(int i = 0; i < c; ++i) {
            System.out.println(i);
        }

    }

    public static int add(int x, int y) {
        return x + y;
    }
}

命令行中获取字节码指令 javap -v Hello.class

看下面的指令的时候,哪个指令不知道啥意思,就到这几个链接里查一下,网上找了几篇,官网没找到惭愧。 Java字节码指令收集大全 , 字节码指令

下面开始读下面的指令到底是啥意思

D:\androiddemo\6\ByteCode\lib\build\classes\java\main\com\hsm\lib>javap -v Hello.class
Classfile /D:/androiddemo/6/ByteCode/lib/build/classes/java/main/com/hsm/lib/Hello.class
  Last modified 2019-12-11; size 1085 bytes
  MD5 checksum 0cca87a281db7758d1c26d2113c3f3ae
  Compiled from "Hello.java"
public class com.hsm.lib.Hello
  minor version: 0  //次版本号
  major version: 51  //主版本号 52代表jdk1.8 , 51代表jdk1.7
  flags: ACC_PUBLIC, ACC_SUPER  //public类型 , 使用新的invokespecial语义
//常量池(一个类的结构索引,包括类,变量,方法名,方法参数等等,供后面的方法直接引用)
Constant pool: 
  //Object类型的构造方法,指向13和14位置  <init>代表构造方法 ()V 代表没有参数,没有返回值
   #1 = Methodref          #13.#41 // java/lang/Object."<init>":()V  
   //add方法,指向12和42位置,参数为两个int
   #2 = Methodref          #12.#42 // com/hsm/lib/Hello.add:(II)I 
   //String类型字面量
   #3 = String             #43  // abc 
   // 字段符号引用,指向java/lang/System这个类的out成员变量,变量类型是Ljava/io/PrintStream
   #4 = Fieldref           #44.#45  // java/lang/System.out:Ljava/io/PrintStream;
   //StringBuilder类
   #5 = Class              #46  // java/lang/StringBuilder  
   //相当于new一个StringBuilder,StringBuilder类的构造法描述 没参数 没有返回值
   #6 = Methodref          #5.#41  // java/lang/StringBuilder."<init>":()V 
   //调用StringBuilder的append方法,参数为int
   #7 = Methodref          #5.#47  // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   //调用StringBuilder的append方法,参数为String
   #8 = Methodref          #5.#48  // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
   //调用toString操作
   #9 = Methodref          #5.#49  // java/lang/StringBuilder.toString:()Ljava/lang/String;
   //方法println,参数为String,没有返回值
  #10 = Methodref          #50.#51 // java/io/PrintStream.println:(Ljava/lang/String;)V 
  //方法println,参数为int,没有返回
  #11 = Methodref          #50.#52 // java/io/PrintStream.println:(I)V 
  #12 = Class              #53  // com/hsm/lib/Hello //Hello类
  #13 = Class              #54  // java/lang/Object  //Hello父类Object类
  #14 = Utf8               <init>  //构造方法
  #15 = Utf8               ()V   //没有参数,没有返回值
  #16 = Utf8               Code  //Code属性表,用来存放JVM指令
  //行号表(保存JVM指令和源码的映射关系,程序抛异常的时候,可以定位哪一行源码报错)
  #17 = Utf8               LineNumberTable  
  #18 = Utf8               LocalVariableTable //局部变量表
  #19 = Utf8               this  //传过来的局部变量 this
  #20 = Utf8               Lcom/hsm/lib/Hello; //描述属于哪个类
  #21 = Utf8               main  //main方法的方法名
  #22 = Utf8               ([Ljava/lang/String;)V  //main方法的描述(参数是String数组,返回是空)
  #23 = Utf8               i  //局部变量i
  #24 = Utf8               I  //i是int类型
  #25 = Utf8               args //局部变量args
  #26 = Utf8               [Ljava/lang/String; //args 是String数组类型
  #27 = Utf8               a  //局部变量a
  #28 = Utf8               b  //局部变量b
  #29 = Utf8               c  //局部变量c
  #30 = Utf8               d  //局部变量d
  #31 = Utf8               Ljava/lang/String; // d是String类型
  //栈图表 记录了局部变量的验证类型,字节码的偏移量,操作栈中的验证类型等
  #32 = Utf8               StackMapTable  
  #33 = Class              #26            // "[Ljava/lang/String;"  //String 数组类
  #34 = Class              #55            // java/lang/String       //String类
  #35 = Utf8               add            //方法名 add
  #36 = Utf8               (II)I      //add方法两个int参数,返回值为int
  #37 = Utf8               x       //局部变量 x
  #38 = Utf8               y       //局部变量 y
  #39 = Utf8               SourceFile  //源码文件名称
  #40 = Utf8               Hello.java  //源码文件名称 Hello.java
  //NameAndType结构用来描述一个方法或者成员变量 ,这里表示构造方法,没有参数,返回值为空
  #41 = NameAndType        #14:#15        // "<init>":()V 
  #42 = NameAndType        #35:#36        // add:(II)I  //add方法,参数为两个int,返回值为int
  #43 = Utf8               abc  //局部变量abc
  #44 = Class              #56            // java/lang/System  //java/lang/System类
  //System类中out属性,out的类型是java/io/PrintStream
  #45 = NameAndType        #57:#58        // out:Ljava/io/PrintStream; 
  #46 = Utf8               java/lang/StringBuilder    //StringBuilder变量
  //append方法,参数是int,返回值是StringBuilder
  #47 = NameAndType        #59:#60        // append:(I)Ljava/lang/StringBuilder;
  //append方法,参数是String,返回值是StringBuilder
  #48 = NameAndType        #59:#61        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 
  //toString方法,参数是空,返回值是String
  #49 = NameAndType        #62:#63        // toString:()Ljava/lang/String; 
  #50 = Class              #64            // java/io/PrintStream  //PrintStream类
  //println方法,参数是String,返回值是空
  #51 = NameAndType        #65:#66        // println:(Ljava/lang/String;)V   
  //println方法,参数是int,返回值是空  这个是源码中for循环中的println
  #52 = NameAndType        #65:#67        // println:(I)V  
  #53 = Utf8               com/hsm/lib/Hello  
  #54 = Utf8               java/lang/Object
  #55 = Utf8               java/lang/String
  #56 = Utf8               java/lang/System
  #57 = Utf8               out
  #58 = Utf8               Ljava/io/PrintStream;
  #59 = Utf8               append
  #60 = Utf8               (I)Ljava/lang/StringBuilder;
  #61 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #62 = Utf8               toString
  #63 = Utf8               ()Ljava/lang/String;
  #64 = Utf8               java/io/PrintStream
  #65 = Utf8               println
  #66 = Utf8               (Ljava/lang/String;)V
  #67 = Utf8               (I)V
{
  public com.hsm.lib.Hello();  //构造方法
    descriptor: ()V  //方法描述 没有参数 返回值为空
    flags: ACC_PUBLIC   // 是public方法
    Code:  //Code属性表,用来存放JVM指令
    //stack 最大操作数栈 1  
    //locals局部变量所需空间这里就一个this 
    //args_size 方法参数的个数,这里是1,因为每个实例方法(new)都会有一个隐藏参数this
      stack=1, locals=1, args_size=1  
         //从局部变量0中加载引用变量入栈
         0: aload_0
         //编译时的方法绑定调用时的方法 这里就是绑定父类的构造方法
         1: invokespecial #1 // Method java/lang/Object."<init>":()V
         //返回空
         4: return
     //行号表(保存JVM指令和源码的映射关系,程序抛异常的时候,可以定位哪一行源码报错)
      LineNumberTable:
        line 8: 0
    // 局部变量表,这里就一个this局部变量
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/hsm/lib/Hello;

  public static void main(java.lang.String[]); //main方法
    descriptor: ([Ljava/lang/String;)V  //参数是String数组,返回为空
    flags: ACC_PUBLIC, ACC_STATIC  //public方法 静态方法
    //Code属性表,用来存放JVM指令
    Code:
     //stack 最大操作数栈 3 JVM运行时会根据这个值来分配栈帧(Frame)中的操作栈深度
     //locals局部变量所需空间 6(args,a,b,c,d,i)
     //args_size 方法参数的个数,这里是1 args
      stack=3, locals=6, args_size=1
         0: iconst_1  //int类型常量1入栈 
         1: istore_1  //将栈顶的int类型数值存入第二个局部变量 a
         2: iconst_2  //int类型常量2入栈
         3: istore_2  //栈顶int数值存入第3个局部变量 b
         4: iload_1   //第2个int型变量进栈 
         5: iload_2   //第3个int型变量进栈
         6: invokestatic  #2  // Method add:(II)I //调用静态方法add
         9: istore_3  //栈顶int数值存入第4局部变量
        10: ldc           #3  // String abc //常量池中的常量入栈
        12: astore        4   //将栈顶引用型数值存入指定本地变量 abc存入变量d
        //获取静态字段,并将其压入栈顶 out
        14: getstatic     #4  // Field java/lang/System.out:Ljava/io/PrintStream;
        //创建一个对象,并将其压入栈顶
        17: new           #5  // class java/lang/StringBuilder
        20: dup   //复制栈顶数值,并压入栈
        //调用需要特殊处理的实例方法 StringBuilder的构造方法
        21: invokespecial #6  // Method java/lang/StringBuilder."<init>":()V
        24: iload_3  // 第4个int型变量进栈 c
        //调用实例的方法 StringBuilder的append(int)方法
        25: invokevirtual #7 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        28: aload         4   //第一个引用类型变量入栈 d
        //调用实例的方法 StringBuilder的append(String)方法
        30: invokevirtual #8  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        //调用实例StringBuilder的toString方法
        33: invokevirtual #9  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        //调用实例的方法  打印的方法PrintStream.println
        36: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        39: iconst_0  //int常量类型0入栈 i
        40: istore        5  //栈顶第一个int数值存入第一个变量
        42: iload         5  //从局部变量indexbyte中装载int类型值入栈
        44: iload_3          //第四个int类型变量入栈
        45: if_icmpge     62  //若栈顶两int类型值前大于等于后则跳转。
        // //获取静态字段,并将其压入栈顶 out
        48: getstatic     #4  // Field java/lang/System.out:Ljava/io/PrintStream;
        51: iload         5  /从局部变量indexbyte中装载int类型值入栈
        //执行打印方法
        53: invokevirtual #11 // Method java/io/PrintStream.println:(I)V
        56: iinc          5, 1 //将指定 int 型变量增加指定值 i++,
        59: goto          42   //无条件跳转到指定位置
        62: return        //void函数返回
    //行号表(保存JVM指令和源码的映射关系,程序抛异常的时候,可以定位哪一行源码报错)
      LineNumberTable:
        line 11: 0
        line 12: 2
        line 13: 4
        line 14: 10
        line 15: 14
        line 16: 39
        line 17: 48
        line 16: 56
        line 19: 62
        //局部变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
           42      20     5     i   I
            0      63     0  args   [Ljava/lang/String;
            2      61     1     a   I
            4      59     2     b   I
           10      53     3     c   I
           14      49     4     d   Ljava/lang/String;
      //栈图表  通过记录偏移量来保证字节序,并且不会重复记录
      StackMapTable: number_of_entries = 2
        frame_type = 255 /* full_frame */
          offset_delta = 42
          locals = [ class "[Ljava/lang/String;", int, int, int, class java/lang/String, int ]
          stack = []
        frame_type = 250 /* chop */
          offset_delta = 19
  public static int add(int, int);  //add 方法
    descriptor: (II)I  //描述:参数两个int值,返回值int
    flags: ACC_PUBLIC, ACC_STATIC  //public static
     //Code属性表,用来存放JVM指令
    Code:
     //stack 最大操作数栈 2  
     //locals局部变量所需空间这2
     //args_size 方法参数的个数 2
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn
      //行号表(保存JVM指令和源码的映射关系,程序抛异常的时候,可以定位哪一行源码报错)
      LineNumberTable:
        line 23: 0
      //局部变量表
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0     x   I
            0       4     1     y   I
}
SourceFile: "Hello.java"

终于一行一行读完了,总结一下,感觉主要有两大部分:

第一大部分常量池(Constant pool)部分这个主要是用来记录各种常量的,不止是我们定义的各种成员变量、局部变量,还有类名类是否有参数,方法名,方法是否有参数,方法的返回值,还有每个类型是用什么数据类型保存的等这个类的所有信息,供后面的方法调用。

第二大部分方法的执行:方法部分有方法的描述,JVM指令执行顺序,每个指令的意思就得去查指令表了。行号表可以保存JVM指令和源码的映射关系,程序抛异常的时候,可以定位哪一行源码报错,局部变量表存放方法中的局部变量。

其实上面的指令已经是翻译过的了,如果直接用 “010 Editor” 这个编辑器查看.class文件,会看到一堆十六进制的字符。他们也都有自己的意思,这个链接这个视频可以了解怎么翻译十六进制的字符,每个字符都有自己的含义,一个一个的查表,翻译完之后就是前面的指令代码了。不过玩ASM的话看懂前面的指令码就可以了。

资料:

为什么要推荐大家学习字节码?

Java虚拟机jvm字节码详细解析

轻松看懂Java字节码

深入理解JVM之Java字节码(.class)文件详解