JVM笔记(2)-笑谈Java字节码指令

基础不牢,地动山摇,Java仍然是业界主流的开发语言之一,Java生态圈中有大量的组件框架,也包括大量大数据的组件如Hadoop等。我们想要更熟练透彻地掌握这些组件框架并更好地开发自己的程序,深入学习JVM的基础很有必要。本篇深入浅出来讲述Java字节码指令运行的过程,避免过度深入太多细节,让学习者可以对JVM解析字节码以及运行指令的过程有一个宏观的认识,对继续深入学习JVM相关知识有总体的把控。

1.准备程序和字节码

Java:

public class ByteCodeT {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void hi() {
        String s12 = "hi ".concat(name);
        hello(s12);
    }

    public String hello(String s2) {
        return s2;
    }

    public String getToken() {
        return UUID.randomUUID()
                .toString()
                .replace("-", "")
                + name.hashCode();
    }
}

在class文件目录输入命令打印出字节码(-v是打印全面字节码信息,-p是涵盖所有成员):

javap -v -p ByteCodeT

字节码:

警告: 二进制文件ByteCodeT包含com.lims.pracpro.jdkprac.ByteCodeT
Classfile /E:/Code/flickeringproject/pracpro/target/classes/com/lims/pracpro/jdkprac/ByteCodeT.class
  Last modified 2020-9-17; size 1319 bytes
  MD5 checksum 1c5e715a75b59bba2b0228e7757fa33d
  Compiled from "ByteCodeT.java"
public class com.lims.pracpro.jdkprac.ByteCodeT
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #18.#40        // java/lang/Object."<init>":()V
   #2 = Fieldref           #17.#41        // com/lims/pracpro/jdkprac/ByteCodeT.name:Ljava/lang/String;
   #3 = String             #42            // hi
   #4 = Methodref          #43.#44        // java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String;
   #5 = Methodref          #17.#45        // com/lims/pracpro/jdkprac/ByteCodeT.hello:(Ljava/lang/String;)Ljava/lan
g/String;
   #6 = Class              #46            // java/lang/StringBuilder
   #7 = Methodref          #6.#40         // java/lang/StringBuilder."<init>":()V
   #8 = Methodref          #47.#48        // java/util/UUID.randomUUID:()Ljava/util/UUID;
   #9 = Methodref          #47.#49        // java/util/UUID.toString:()Ljava/lang/String;
  #10 = String             #50            // -
  #11 = String             #51            //
  #12 = Methodref          #43.#52        // java/lang/String.replace:(Ljava/lang/CharSequence;Ljava/lang/CharSeque
nce;)Ljava/lang/String;
  #13 = Methodref          #6.#53         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBu
ilder;
  #14 = Methodref          #43.#54        // java/lang/String.hashCode:()I
  #15 = Methodref          #6.#55         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
  #16 = Methodref          #6.#49         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #17 = Class              #56            // com/lims/pracpro/jdkprac/ByteCodeT
  #18 = Class              #57            // java/lang/Object
  #19 = Utf8               name
  #20 = Utf8               Ljava/lang/String;
  #21 = Utf8               <init>
  #22 = Utf8               ()V
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               LocalVariableTable
  #26 = Utf8               this
  #27 = Utf8               Lcom/lims/pracpro/jdkprac/ByteCodeT;
  #28 = Utf8               getName
  #29 = Utf8               ()Ljava/lang/String;
  #30 = Utf8               setName
  #31 = Utf8               (Ljava/lang/String;)V
  #32 = Utf8               hi
  #33 = Utf8               s12
  #34 = Utf8               hello
  #35 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #36 = Utf8               s2
  #37 = Utf8               getToken
  #38 = Utf8               SourceFile
  #39 = Utf8               ByteCodeT.java
  #40 = NameAndType        #21:#22        // "<init>":()V
  #41 = NameAndType        #19:#20        // name:Ljava/lang/String;
  #42 = Utf8               hi
  #43 = Class              #58            // java/lang/String
  #44 = NameAndType        #59:#35        // concat:(Ljava/lang/String;)Ljava/lang/String;
  #45 = NameAndType        #34:#35        // hello:(Ljava/lang/String;)Ljava/lang/String;
  #46 = Utf8               java/lang/StringBuilder
  #47 = Class              #60            // java/util/UUID
  #48 = NameAndType        #61:#62        // randomUUID:()Ljava/util/UUID;
  #49 = NameAndType        #63:#29        // toString:()Ljava/lang/String;
  #50 = Utf8               -
  #51 = Utf8
  #52 = NameAndType        #64:#65        // replace:(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/S
tring;
  #53 = NameAndType        #66:#67        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #54 = NameAndType        #68:#69        // hashCode:()I
  #55 = NameAndType        #66:#70        // append:(I)Ljava/lang/StringBuilder;
  #56 = Utf8               com/lims/pracpro/jdkprac/ByteCodeT
  #57 = Utf8               java/lang/Object
  #58 = Utf8               java/lang/String
  #59 = Utf8               concat
  #60 = Utf8               java/util/UUID
  #61 = Utf8               randomUUID
  #62 = Utf8               ()Ljava/util/UUID;
  #63 = Utf8               toString
  #64 = Utf8               replace
  #65 = Utf8               (Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
  #66 = Utf8               append
  #67 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #68 = Utf8               hashCode
  #69 = Utf8               ()I
  #70 = Utf8               (I)Ljava/lang/StringBuilder;
{
  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

  public com.lims.pracpro.jdkprac.ByteCodeT();
    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 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;

  public java.lang.String getName();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field name:Ljava/lang/String;
         4: areturn
      LineNumberTable:
        line 15: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;

  public void setName(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field name:Ljava/lang/String;
         5: return
      LineNumberTable:
        line 19: 0
        line 20: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
            0       6     1  name   Ljava/lang/String;

  public void hi();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=1
         0: ldc           #3                  // String hi
         2: aload_0
         3: getfield      #2                  // Field name:Ljava/lang/String;
         6: invokevirtual #4                  // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/Stri
ng;
         9: astore_1
        10: aload_0
        11: aload_1
        12: invokevirtual #5                  // Method hello:(Ljava/lang/String;)Ljava/lang/String;
        15: pop
        16: return
      LineNumberTable:
        line 23: 0
        line 24: 10
        line 25: 16
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      17     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
           10       7     1   s12   Ljava/lang/String;

  public java.lang.String hello(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: aload_1
         1: areturn
      LineNumberTable:
        line 28: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       2     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
            0       2     1    s2   Ljava/lang/String;

  public java.lang.String getToken();
    descriptor: ()Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=1, args_size=1
         0: new           #6                  // class java/lang/StringBuilder
         3: dup
         4: invokespecial #7                  // Method java/lang/StringBuilder."<init>":()V
         7: invokestatic  #8                  // Method java/util/UUID.randomUUID:()Ljava/util/UUID;
        10: invokevirtual #9                  // Method java/util/UUID.toString:()Ljava/lang/String;
        13: ldc           #10                 // String -
        15: ldc           #11                 // String
        17: invokevirtual #12                 // Method java/lang/String.replace:(Ljava/lang/CharSequence;Ljava/lan
g/CharSequence;)Ljava/lang/String;
        20: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/la
ng/StringBuilder;
        23: aload_0
        24: getfield      #2                  // Field name:Ljava/lang/String;
        27: invokevirtual #14                 // Method java/lang/String.hashCode:()I
        30: invokevirtual #15                 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;

        33: invokevirtual #16                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        36: areturn
      LineNumberTable:
        line 32: 0
        line 33: 10
        line 34: 17
        line 35: 27
        line 32: 36
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      37     0  this   Lcom/lims/pracpro/jdkprac/ByteCodeT;
}
SourceFile: "ByteCodeT.java"
View Code

可以看出,几个方法的复杂度:getName<setName<hi<getToken;我们一步一步来看。

选择方法getName():

 

 一共三行指令:

 第一行,aload_0,看起来应该是加载东西的,先不管;

第二行,getfield,后面引用了常量池的2号常量,查看常量池可知,2号常量是字段引用(即为String name)

 第三行,areturn,即是return返回。

仔细想来,getfield指令传入了参数“Field name:Ljava/lang/String;”并不完整,后半部分只是Ljava/lang/String类型,整个过程究竟取谁的name值?这里get的是谁的field?

由此可以想到第一步的指令是为第二步的取值准备了数据。

结合局部变量表来看,aload_0正是取了局部变量表中第0个槽的数据,意思是将Slot 0中的值压入栈中,其值为this,就是当前类的实例,即getfield的主体。

2.解析

Jvm大帝是神之旨意的履行者(Jvm大帝就是虚拟机,神就是开发者,神之旨意是开发者写好并编译后的字节码...),当Jvm大帝带领Java世界运行进入了一个新的方法后,会为这个方法在栈内存大陆上创造两个重要的领域:局部变量表操作数栈

局部变量表里一般会包含this指针(针对实例方法,静态方法当然无此)、方法的所有传入参数方法中所开辟的本地变量

我们再引入另外一个比喻,如果把运行Java方法理解为拍戏,那么局部变量表里的各个局部变量就是这部戏的核心主角,或者说领衔主演,而操作数栈正是这部戏的舞台。所谓操作数栈搭台,局部变量唱戏,是也。那么aload_0就是告诉Jvm导演(大帝已沦落为导演),请0号演员this同志登台(压栈),演后边的本子。

当然了,这个比喻并不完全恰当,因为操作数栈并不是“舞台”的结构,而是栈的结构。但是这个比喻可以很好地说明局部变量表和操作数栈之间的关系,以及aload_0的作用。

 getfield做的操作:this入栈——》弹出this——》取值

3.方法执行深入

方法执行的操作:

 

上图展示指令操作数栈局部变量表三者的运作关系

关于字节码指令,一是指令的功能,二是指令操作的数据类型。先从功能说起,指令主要可以分为如下几类:

  1. 存储和加载类指令:主要包括load系列指令、store系列指令和ldc、push系列指令,主要用于在局部变量表、操作数栈和常量池三者之间进行数据调度;(关于常量池前面没有特别讲解,这个也很简单,顾名思义,就是这个池子里放着各种常量,好比片场的道具库)
  2. 对象操作指令(创建与读写访问):比如我们刚刚的putfield和getfield就属于读写访问的指令,此外还有putstatic/getstatic,还有new系列指令,以及instanceof等指令。
  3. 操作数栈管理指令:如pop和dup,他们只对操作数栈进行操作。
  4. 类型转换指令和运算指令:如add/div/l2i等系列指令,实际上这类指令一般也只对操作数栈进行操作。
  5. 控制跳转指令:这类里包含常用的if系列指令以及goto类指令。
  6. 方法调用和返回指令:主要包括invoke系列指令和return系列指令。这类指令也意味这一个方法空间的开辟和结束,即invoke会唤醒一个新的java方法小宇宙(新的栈和局部变量表),而return则意味着这个宇宙的结束回收。

指令操作的数据类型来讲:指令开头或尾部的一些字母,就往往表明了它所能操作的数据类型:

a对应对象,表示指令操作对象性数据,比如aload和astore、areturn等等。
i对应整形。也就有iload,istore等i系列指令。
f对应浮点型。
l对应long,b对应byte,d对应double,c对应char。
另外地,ia对应int array,aa对应object array,da对应double array。不在一一赘述。

解析复杂方法:

 解析:

  1. (红色第1部分)new指令执行,引用常量池6号参数,创建一个StringBuilder对象,在内存中开辟空间(并将其引用入栈),用于实现连接字符串功能,类似C++中的运算符重载。
  2. dup指令执行,复制栈顶,并入栈,此时栈深为2(2个引用)——(一般new指令后会跟dup复制一份引用,其中一个引用用于init初始化,一个引用给程序员用于赋值等使用
  3. invokespecial指令执行,使用常量池7号符号引用(<init>方法初始化),此时用到了栈顶的1个引用参数,栈深变为1。
  4. (黄色第2部分)invokestatic指令执行,调用UUID.randomUUID()静态方法,结果压栈,并弹出,调用String的toString方法,结果再压栈,此时栈深2。
  5. (绿色第3部分)字符串“-”、“”入栈,栈深为4,弹出栈顶3个元素,调用String的replace方法,最后一个元素调用方,其余两个为参数,结果入栈,此时栈深2。
  6. (青色第5部分)弹出栈顶2个元素,调用StringBuilder的append方法,最后一个元素为调用方,先弹出的为参数,结果入栈,栈深为1。
  7. (粉色第5部分)aload_0指令执行,将局部变量表的第0个变量压栈(this入栈),栈深为2,;弹出栈顶调用方,调用getfield方法,结果入栈,栈深为2;弹出栈顶调用方,调用hashCode方法,结果入栈,栈深为2。
  8. (洋红第6部分)弹出栈顶2个元素,调用StringBuilder的append方法,最后弹出元素为调用方,先弹出元素为append参数,结果入栈,栈深1;弹出栈顶,调用toString方法,结果压栈,栈深1。
  9. areturn返回栈顶,栈空,结果返回,方法调用完成。

原文地址:https://www.cnblogs.com/limaosheng/p/13685613.html