Java字节码(Java bytecode)

291 阅读11分钟

java字节码,就是jvm执行的一种指令格式.jvm通过字节码指令,做相对应的动作

字节码查看(助记符->二进制)

.class文件本身是二进制文件,我们可以以两种方式查看,一种就是直接看它的二进制内容,但是不方便查看和理解,还有一种是看它经过javap转成助记符模式的内容,方便我们理解

二进制查看方式

二进制文件查看器

因为电脑中的二进制文件,一般文本编辑器在打开的同时会给做一些转义,这里推荐使用比如winhex这种软件查看,便于理解

助记符查看

javap

使用javac命令可以将.java文件编译成.class文件,这个.class文件,就是所谓的java字节码文件.

使用javap -c命令可以查看.class文件的字节码.

使用javap -c -verbose命令可以查看字节码的详细信息.

IDEA 插件

jclasslib Bytecode Viewer 插件,可以方便查看每个java类编译后的字节码文件

简单的字节码解析

编译前java代码

package com.rrtx.adm;

/**
 * Created by yarne on 2021/7/3.
 */
public class Main {
    public static void main(String[] args) {
        int a = 5;
        double b = 2.00;
        String c = "3";
        add(c,a,b);
    }
    public static void add(String a,int b,double c){
        double v = Integer.valueOf(a) + b + c;
        System.out.println(v);
    }
}

查看.class的字节码(助记符)

Classfile /C:/Users/14641/Desktop/Main.class
  Last modified 2021-7-4; size 890 bytes
  MD5 checksum 683b4cdcd60108ce5bfdcc9e6fe3c992
  Compiled from "Main.java"
public class com.rrtx.adm.Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #11.#34        // java/lang/Object."<init>":()V
   #2 = Double             2.0d
   #4 = String             #35            // 3
   #5 = Methodref          #10.#36        // com/rrtx/adm/Main.add:(Ljava/lang/String;ID)V
   #6 = Methodref          #37.#38        // java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
   #7 = Methodref          #37.#39        // java/lang/Integer.intValue:()I
   #8 = Fieldref           #40.#41        // java/lang/System.out:Ljava/io/PrintStream;
   #9 = Methodref          #42.#43        // java/io/PrintStream.println:(D)V
  #10 = Class              #44            // com/rrtx/adm/Main
  #11 = Class              #45            // java/lang/Object
  #12 = Utf8               <init>
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               Lcom/rrtx/adm/Main;
  #19 = Utf8               main
  #20 = Utf8               ([Ljava/lang/String;)V
  #21 = Utf8               args
  #22 = Utf8               [Ljava/lang/String;
  #23 = Utf8               a
  #24 = Utf8               I
  #25 = Utf8               b
  #26 = Utf8               D
  #27 = Utf8               c
  #28 = Utf8               Ljava/lang/String;
  #29 = Utf8               add
  #30 = Utf8               (Ljava/lang/String;ID)V
  #31 = Utf8               v
  #32 = Utf8               SourceFile
  #33 = Utf8               Main.java
  #34 = NameAndType        #12:#13        // "<init>":()V
  #35 = Utf8               3
  #36 = NameAndType        #29:#30        // add:(Ljava/lang/String;ID)V
  #37 = Class              #46            // java/lang/Integer
  #38 = NameAndType        #47:#48        // valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
  #39 = NameAndType        #49:#50        // intValue:()I
  #40 = Class              #51            // java/lang/System
  #41 = NameAndType        #52:#53        // out:Ljava/io/PrintStream;
  #42 = Class              #54            // java/io/PrintStream
  #43 = NameAndType        #55:#56        // println:(D)V
  #44 = Utf8               com/rrtx/adm/Main
  #45 = Utf8               java/lang/Object
  #46 = Utf8               java/lang/Integer
  #47 = Utf8               valueOf
  #48 = Utf8               (Ljava/lang/String;)Ljava/lang/Integer;
  #49 = Utf8               intValue
  #50 = Utf8               ()I
  #51 = Utf8               java/lang/System
  #52 = Utf8               out
  #53 = Utf8               Ljava/io/PrintStream;
  #54 = Utf8               java/io/PrintStream
  #55 = Utf8               println
  #56 = Utf8               (D)V
{
  public com.rrtx.adm.Main();
    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 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/rrtx/adm/Main;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=5, args_size=1
         0: iconst_5
         1: istore_1
         2: ldc2_w        #2                  // double 2.0d
         5: dstore_2
         6: ldc           #4                  // String 3
         8: astore        4
        10: aload         4
        12: iload_1
        13: dload_2
        14: invokestatic  #5                  // Method add:(Ljava/lang/String;ID)V
        17: return
      LineNumberTable:
        line 8: 0
        line 9: 2
        line 10: 6
        line 11: 10
        line 12: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            2      16     1     a   I
            6      12     2     b   D
           10       8     4     c   Ljava/lang/String;

  public static void add(java.lang.String, int, double);
    descriptor: (Ljava/lang/String;ID)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=6, args_size=3
         0: aload_0
         1: invokestatic  #6                  // Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
         4: invokevirtual #7                  // Method java/lang/Integer.intValue:()I
         7: iload_1
         8: iadd
         9: i2d
        10: dload_2
        11: dadd
        12: dstore        4
        14: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: dload         4
        19: invokevirtual #9                  // Method java/io/PrintStream.println:(D)V
        22: return
      LineNumberTable:
        line 16: 0
        line 17: 14
        line 18: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0     a   Ljava/lang/String;
            0      23     1     b   I
            0      23     2     c   D
           14       9     4     v   D
}

结构说明

简单将上边的字节码文件做分类,可以将一个类分成class摘要、常量池、方法栈帧这几部分

class摘要

摘要主要就是记录class的一些基本信息,包括类的大小修改时间,校验和,编译时候的JDK版本信息,以及类的访问范围等

Classfile /C:/Users/yarne/Desktop/Main.class    //来源于解析桌面Main.class文件
  Last modified 2021-7-4; size 890 bytes        //最后修改时间以及类大小
  MD5 checksum 683b4cdcd60108ce5bfdcc9e6fe3c992 //校验和
  Compiled from "Main.java"
public class com.rrtx.adm.Main
  minor version: 0                             //jdk 子版本号
  major version: 52							   //jdk 主版本号   52代表是java8
  flags: ACC_PUBLIC, ACC_SUPER                 //public类,初始化方法使用父类的

flags标识符对应的含义

标志符名称标志符值释义
ACC_PUBLIC0x0001Public 类型
ACC_FINAL0x0010Final类型
ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义
ACC_INTERFACE0x0200接口修饰符
ACC_ABSTRACT0x0400abstract修饰符
ACC_SYNTHETIC0x1000标志这个类并非由用户代码生成
ACC_ANNOTATION0x2000注解修饰符
**ACC_ENUM0x400枚举修饰符

常量池

常量池主要就是包含一个类中所有的基本类型常量以及符号引用,基本类型常量包括字符串常量、final修饰的成员变量、实例变量等,符号引用包括类和接口的名称、字段的名称和描述、方法的名称和描述

基本类型常量

基本类型常量里面,主要包含的是字面量和引用符,字面量的含义包括文本字符串,final修饰的成员变量,还有数据的值等,对于基本数值int类型的常量,常量池值保存了引用和字面名称,没有保存数据的值

#2 = Double             2.0d		   //double类型的存了实际值
#4 = String             #35            // 引用#35
#35 = Utf8               3             // new String("3")
#23 = Utf8               a            //int类型只存了名称,没有存实际值
#24 = Utf8               I

符号引用

符号引用就包含了很多类、接口的名称和描述,字段的名称和描述,方法的名称和描述等等

   #5 = Methodref          #10.#36        // com/rrtx/adm/Main.add:(Ljava/lang/String;ID)V
   #6 = Methodref          #37.#38        // java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
   #7 = Methodref          #37.#39        // java/lang/Integer.intValue:()I
   #8 = Fieldref           #40.#41        // java/lang/System.out:Ljava/io/PrintStream;
   #9 = Methodref          #42.#43        // java/io/PrintStream.println:(D)V
  #10 = Class              #44            // com/rrtx/adm/Main
  #11 = Class              #45            // java/lang/Object

方法栈帧

方法栈帧就是类里面的方法,每个方法都是一帧,它在类编译之后就被定义好,在线程运行时根据定义进行创建,调用结束后被销毁。

jvm所有的计算操作都是基于栈完成的,每个线程创建出来的同时,都会独享一个自己的线程栈,这个线程栈的作用就是存储栈帧,当前线程中调用的每个方法,jvm都会创建出一个栈帧,每个栈帧中储存着局部变量表、操作栈、动态链接、返回地址以及一些额外的附加摘要信息,栈帧在被调用结束之后销毁

下面是Main.class里所有的栈帧,一共两个方法。

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=5, args_size=1
         0: iconst_5
         1: istore_1
         2: ldc2_w        #2                  // double 2.0d
         5: dstore_2
         6: ldc           #4                  // String 3
         8: astore        4
        10: aload         4
        12: iload_1
        13: dload_2
        14: invokestatic  #5                  // Method add:(Ljava/lang/String;ID)V
        17: return
      LineNumberTable:
        line 8: 0
        line 9: 2
        line 10: 6
        line 11: 10
        line 12: 17
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;
            2      16     1     a   I
            6      12     2     b   D
           10       8     4     c   Ljava/lang/String;

  public static void add(java.lang.String, int, double);
    descriptor: (Ljava/lang/String;ID)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=6, args_size=3
         0: aload_0
         1: invokestatic  #6                  // Method java/lang/Integer.valueOf:(Ljava/lang/String;)Ljava/lang/Integer;
         4: invokevirtual #7                  // Method java/lang/Integer.intValue:()I
         7: iload_1
         8: iadd
         9: i2d
        10: dload_2
        11: dadd
        12: dstore        4
        14: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        17: dload         4
        19: invokevirtual #9                  // Method java/io/PrintStream.println:(D)V
        22: return
      LineNumberTable:
        line 16: 0
        line 17: 14
        line 18: 22
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      23     0     a   Ljava/lang/String;
            0      23     1     b   I
            0      23     2     c   D
           14       9     4     v   D
}

栈帧摘要

从栈帧的摘要信息中,基本上可以大概知道一个方法的基本信息

 //main方法
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC       //静态的,公共方法
    Code:
      stack=4, locals=5, args_size=1    //栈帧的深度是4,最大局部变量是5个,入参数量是1个
                  

// add方法 
 public static void add(java.lang.String, int, double);
    descriptor: (Ljava/lang/String;ID)V
    flags: ACC_PUBLIC, ACC_STATIC        //静态的,公共方法
    Code:
      stack=4, locals=6, args_size=3     //栈帧的深度是4,最大局部变量是6个,入参数量是3个

局部变量表

局部变量表是一个数组,用来存储当前方法的局部变量,表中可以存储的类型包括boolean、byte、char、short、int、float以及引用类型,因为局部变量是线程私有的,所以不会UC你在线程安全问题,每个栈帧中本地变量表的大小,在.class字节码文件编译完成之后,就已经确定

// main方法 局部变量表
LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      18     0  args   [Ljava/lang/String;  //入参args,类型是String数组
            2      16     1     a   I                 // 局部变量a,类型是I  int
            6      12     2     b   D                 //局部变量b,类型是D double
           10       8     4     c   Ljava/lang/String; //局部变量c,类型是String

 // add方法 局部变量表
LocalVariableTable:
        Start  Length  Slot  Name   Signature           
            0      23     0     a   Ljava/lang/String;   //入参a,类型是String
            0      23     1     b   I					// 入参b,类型是I 
            0      23     2     c   D					//入参c,类型是D
           14       9     4     v   D				    //计算出的值v ,类型是D

操作数栈

操作数栈也是一个用于计算的栈,方法执行从空栈开始,根据字节码指令不停进行入栈计算,然后出栈,栈的大小也是class编译完成就计算完成的。下面简单看一下main方法操作数栈指令执行流程

//main方法的操作数栈
   		 0: iconst_5
         1: istore_1
         2: ldc2_w        #2                  // double 2.0d
         5: dstore_2
         6: ldc           #4                  // String 3
         8: astore        4
        10: aload         4
        12: iload_1
        13: dload_2
        14: invokestatic  #5                  // Method add:(Ljava/lang/String;ID)V
        17: return
指令的分类

根据指令的性质,主要可以分为四个类型:

  1. 栈操作指令,包括与局部变量交互的指令
  2. 程序流程控制指令
  3. 对象操作指令,包括方法调用指令
  4. 算术运算以及类型转换指令
关键性指令load&store

jvm运行过程中,所有的操作都是在虚拟机栈上进行,但是因为栈的生命周期和线程是一致的,每次计算完毕出栈之后,栈就会被销毁掉,不保留任何数据。所以每次栈操作过程中,会有一个关键性动作,就是从本地变量表中先去加载数据,计算完毕之后,再存储回去

常用指令类型(只说几个上边的)
  1. 加载以及存储指令load、store
  2. 常量定义const
  3. ldc 复杂类型定义
  4. 入栈出栈基本类型:iconst、istore、iload中的i,代表意思是int,以次类推
i代表int类型
l代表long
s代表short
b代表byte
c代表char
f代表float
d代表double
a和其他的不一样,a代表的是引用类型
  1. invokestatic 调用静态方法

  2. 出栈 return

操作流程
0: 先定义int类型的常量为5
1: 将这个int类型常量出栈存到slot为1的常量数组里
2: 使用#2引用下的复杂类型定义double类型 2.0
5: 将double类型的常量存到slot为2的常量数组里
6:使用#4引用下的复杂类型定义String类型 字符串3
8: 存到slot为4的常量数组里
10:引用类型载入常量数组中slot为4的值
12:int类型载入常量数组中slot为1的值
13:double类型载入常量数组中slot为2的值
15:调用引用符号为#5的方法帧继续计算
17:出栈

总结

上边简单介绍了字节码的查看方式,字节码文件的结构,以及如何去理解字节码运行结构,上面有提到过,jvm是基于栈计算的,所以了解栈以及运行时结构,可以让我更加深刻的理解jvm栈运行时的动作。

最终用一个比较麻烦的一句话表示线程栈和栈帧之间的调用过程

线程创建->线程栈创建->调用栈帧A->栈帧A创建->栈帧A调用操作数栈->栈帧A操作数栈调用栈帧B->栈帧B创建->栈帧B调用操作数栈->栈帧B返回->栈帧B销毁->栈帧A返回->栈帧A销毁->线程栈销毁->线程销毁

参考:

www.lagou.com/lgeduarticl…

segmentfault.com/a/119000003…

侵权删除