22. java虚拟机总结-从栈帧看字节码 (五)

怎么查看字节码文件?
字节码文件长什么样子?
对象初始化之后,具体的字节码又是怎么执行的?

查看字节码的工具

javap

javap 是 JDK 自带的反解析工具。它的作用是将 .class 字节码文件解析成可读的文件格式

// -v 参数,尽量多打印一些信息, -p 参数,打印一些私有的字段和方法
javap -p -v HelloWorld

问题:某个类中增加一行注释之后,两次生成的 .class 文件,它们的 MD5 是不一样的?答案是否定的
我们知道生成.class的是javac命令,javac 中可以指定一些额外的内容输出到字节码。经常用的有

    javac -g:lines 强制生成 LineNumberTable。
    javac -g:vars  强制生成 LocalVariableTable。
    javac -g 生成所有的 debug 信息。
jclasslib

jclasslib 是一个图形化的工具,能够更加直观的查看字节码中的内容。jclasslib 的下载地址:https://github.com/ingokegel/jclasslib。同时,在Android studio 或者 Idea 中都可以通过插件安装,可以从 plugins 中搜索

There is a plugin for IntelliJ IDEA that can be installed via the plugin manager. The action in the "View menu" can be invoked when a Java, Groovy or Kotlin file is open in the editor.
ij_action.png

下面通过代码,来看一下类加载和对象创建的过程

class B {
    private int a = 1234;

    static long C = 1111;

    public long test(long num) {
        long ret = this.a + num + C;
        return ret;
    }
}

public class A {
    private B b = new B();

    public static void main(String[] args) {
        A a = new A();
        long num = 4321 ;

        long ret = a.b.test(num);

        System.out.println(ret);
    }
}

我们知道对象的创建方式有以下这几种:

使用 Class 的 newInstance 方法。
使用 Constructor 类的 newInstance 方法。
反序列化。
使用 Object 的 clone 方法。

其中,后面两种方式没有调用到构造函数。

当虚拟机遇到一条 new 指令时,首先会检查这个指令的参数能否在常量池中定位一个符号引用。然后检查这个符号引用的类字节码是否加载、解析和初始化。如果没有,将执行对应的类加载过程。

在上边的代码中,在调用 private B b = new B() 时,就会触发 B 类的加载


无名.jpg

A 和 B 会被加载到元空间的方法区,进入 main 方法后,将会交给执行引擎执行。这个执行过程是在栈上完成的,其中有几个重要的区域,包括虚拟机栈、程序计数器等。接下来我们详细看一下虚拟机栈上的执行过程

查看字节码

使用如下命令将Java文件编译成class文件然后解析成操作码

javac -g:lines -g:vars A.java

javap -p -v A
javap -p -v B
renzm:src renzm$ javap -p -v A
Classfile /Users/renzm/IdeaProjects/untitled/src/A.class
  Last modified 2020-7-12; size 619 bytes
  MD5 checksum 4d0a145212532396e853d26eea3d02d4
public class A
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#30        // java/lang/Object."":()V
   #2 = Class              #31            // B
   #3 = Methodref          #2.#30         // B."":()V
   #4 = Fieldref           #5.#32         // A.b:LB;
   #5 = Class              #33            // A
   #6 = Methodref          #5.#30         // A."":()V
   #7 = Long               4321l
   #9 = Methodref          #2.#34         // B.test:(J)J
  #10 = Fieldref           #35.#36        // java/lang/System.out:Ljava/io/PrintStream;
  #11 = Methodref          #37.#38        // java/io/PrintStream.println:(J)V
  #12 = Class              #39            // java/lang/Object
  #13 = Utf8               b
  #14 = Utf8               LB;
  #15 = Utf8               
  #16 = Utf8               ()V
  #17 = Utf8               Code
  #18 = Utf8               LineNumberTable
  #19 = Utf8               LocalVariableTable
  #20 = Utf8               this
  #21 = Utf8               LA;
  #22 = Utf8               main
  #23 = Utf8               ([Ljava/lang/String;)V
  #24 = Utf8               args
  #25 = Utf8               [Ljava/lang/String;
  #26 = Utf8               a
  #27 = Utf8               num
  #28 = Utf8               J
  #29 = Utf8               ret
  #30 = NameAndType        #15:#16        // "":()V
  #31 = Utf8               B
  #32 = NameAndType        #13:#14        // b:LB;
  #33 = Utf8               A
  #34 = NameAndType        #40:#41        // test:(J)J
  #35 = Class              #42            // java/lang/System
  #36 = NameAndType        #43:#44        // out:Ljava/io/PrintStream;
  #37 = Class              #45            // java/io/PrintStream
  #38 = NameAndType        #46:#47        // println:(J)V
  #39 = Utf8               java/lang/Object
  #40 = Utf8               test
  #41 = Utf8               (J)J
  #42 = Utf8               java/lang/System
  #43 = Utf8               out
  #44 = Utf8               Ljava/io/PrintStream;
  #45 = Utf8               java/io/PrintStream
  #46 = Utf8               println
  #47 = Utf8               (J)V
{
  private B b;
    descriptor: LB;
    flags: ACC_PRIVATE

  public A();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: new           #2                  // class B
         8: dup
         9: invokespecial #3                  // Method B."":()V
        12: putfield      #4                  // Field b:LB;
        15: return
      LineNumberTable:
        line 15: 0
        line 16: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      16     0  this   LA;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=6, args_size=1
         0: new           #5                  // class A
         3: dup
         4: invokespecial #6                  // Method "":()V
         7: astore_1
         8: ldc2_w        #7                  // long 4321l
        11: lstore_2
        12: aload_1
        13: getfield      #4                  // Field b:LB;
        16: lload_2
        17: invokevirtual #9                  // Method B.test:(J)J
        20: lstore        4
        22: getstatic     #10                 // Field java/lang/System.out:Ljava/io/PrintStream;
        25: lload         4
        27: invokevirtual #11                 // Method java/io/PrintStream.println:(J)V
        30: return
      LineNumberTable:
        line 19: 0
        line 20: 8
        line 22: 12
        line 24: 22
        line 25: 30
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      31     0  args   [Ljava/lang/String;
            8      23     1     a   LA;
           12      19     2   num   J
           22       9     4   ret   J
}


//////////////////////////////////////////////////////////////


renzm:src renzm$ javap -p -v B
Classfile /Users/renzm/IdeaProjects/untitled/src/B.class
  Last modified 2020-7-12; size 446 bytes
  MD5 checksum 1e8e7a73c9932d559c13e4ae08ee42d0
class B
  minor version: 0
  major version: 52
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#24         // java/lang/Object."":()V
   #2 = Fieldref           #6.#25         // B.a:I
   #3 = Fieldref           #6.#26         // B.C:J
   #4 = Long               1111l
   #6 = Class              #27            // B
   #7 = Class              #28            // java/lang/Object
   #8 = Utf8               a
   #9 = Utf8               I
  #10 = Utf8               C
  #11 = Utf8               J
  #12 = Utf8               
  #13 = Utf8               ()V
  #14 = Utf8               Code
  #15 = Utf8               LineNumberTable
  #16 = Utf8               LocalVariableTable
  #17 = Utf8               this
  #18 = Utf8               LB;
  #19 = Utf8               test
  #20 = Utf8               (J)J
  #21 = Utf8               num
  #22 = Utf8               ret
  #23 = Utf8               
  #24 = NameAndType        #12:#13        // "":()V
  #25 = NameAndType        #8:#9          // a:I
  #26 = NameAndType        #10:#11        // C:J
  #27 = Utf8               B
  #28 = Utf8               java/lang/Object
{
  private int a;
    descriptor: I
    flags: ACC_PRIVATE

  static long C;
    descriptor: J
    flags: ACC_STATIC

  B();
    descriptor: ()V
    flags:
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: aload_0
         5: sipush        1234
         8: putfield      #2                  // Field a:I
        11: return
      LineNumberTable:
        line 1: 0
        line 2: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      12     0  this   LB;

  public long test(long);
    descriptor: (J)J
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=5, args_size=2
         0: aload_0
         1: getfield      #2                  // Field a:I
         4: i2l
         5: lload_1
         6: ladd
         7: getstatic     #3                  // Field C:J
        10: ladd
        11: lstore_3
        12: lload_3
        13: lreturn
      LineNumberTable:
        line 7: 0
        line 8: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   LB;
            0      14     1   num   J
           12       2     3   ret   J

  static {};
    descriptor: ()V
    flags: ACC_STATIC
    Code:
      stack=2, locals=0, args_size=0
         0: ldc2_w        #4                  // long 1111l
         3: putstatic     #3                  // Field C:J
         6: return
      LineNumberTable:
        line 4: 0
}

可视化查看字节码

使用更加直观的工具 jclasslib,以 B.class 文件为例,来查看它的内容

首先,我们能够看到 Constant Pool(常量池),这些内容,就存放于我们的 Metaspace 区域,属于非堆。常量池包含 .class 文件常量池、运行时常量池、String 常量池等部分,大多是一些静态内容。


constant pool.png

接下来,可以看到两个默认的 方法。以下截图是 test 方法的 code 区域,比命令行版的更加直观。

methods.png

继续往下看,我们看到了 LocalVariableTable 的三个变量。其中,slot 0 指向的是 this 关键字。该属性的作用是描述帧栈中局部变量与源码中定义的变量之间的关系。如果没有这些信息,那么在 IDE 中引用这个方法时,将无法获取到方法名,取而代之的则是 arg0 这样的变量名。


test.png

本地变量表的 slot 是可以复用的。注意一个有意思的地方,index 的最大值为 3,证明了本地变量表同时最多能够存放 4 个变量。

另外,我们观察到还有 LineNumberTable 等选项。该属性的作用是描述源码行号与字节码行号(字节码偏移量)之间的对应关系,有了这些信息,在 debug 时,就能够获取到发生异常的源代码行号。

test 函数执行过程
Code 区域介绍

test 函数同时使用了成员变量 a、静态变量 C,以及输入参数 num。我们此时说的函数执行,内存其实就是在虚拟机栈上分配的。下面这些内容,就是 test 方法的字节码。

class B {
    private int a = 1234;
    static long C = 1111;
    public long test(long num) {
        long ret = this.a + num + C;
        return ret;
    }
}

 public long test(long);
    descriptor: (J)J
    flags: ACC_PUBLIC
    Code:
      stack=4, locals=5, args_size=2
         //把第 1 个引用型局部变量推到操作数栈,这里的意思是把 this 装载到了操作数栈中。
        //对于 static 方法,aload_0 表示对方法的第一个参数的操作。
         0: aload_0
        //将栈顶的指定的对象的第 2 个实例域(Field)的值,压入栈顶。#2 就是指的我们的成员变量 a。
         1: getfield      #2                  // Field a:I
        //将栈顶 int 类型的数据转化为 long 类型,这里就涉及我们的隐式类型转换了。图中的信息没有变动,不再详解介绍。
         4: i2l
        //将第一个局部变量入栈。也就是我们的参数 num。这里的 l 表示 long,同样用于局部变量装载。你会看到这个位置的局部变量,一开始就已经有值了。
         5: lload_1
         //把栈顶两个 long 型数值出栈后相加,并将结果入栈。
         6: ladd
         //根据偏移获取静态属性的值,并把这个值 push 到操作数栈上。
         7: getstatic     #3                  // Field C:J
         //把栈顶两个 long 型数值出栈后相加,并将结果入栈。
        10: ladd
        //把栈顶 long 型数值存入第 4 个局部变量。
        11: lstore_3
        //正好与上面相反。上面是变量存入,我们现在要做的,就是把这个变量 ret,压入虚拟机栈中。
        12: lload_3
        //返回栈顶的 long
        13: lreturn
      LineNumberTable:
        line 7: 0
        line 8: 12
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      14     0  this   LB;
            0      14     1   num   J
           12       2     3   ret   J

介绍一下比较重要的 3 三个数值。
1.首先,注意 stack 字样,它此时的数值为 4,表明了 test 方法的最大操作数栈深度为 4。JVM 运行时,会根据这个数值,来分配栈帧中操作栈的深度。
2.相对应的,locals 变量存储了局部变量的存储空间。它的单位是 Slot(槽),可以被重用。其中存放的内容,包括:

this
方法参数
异常处理器的参数
方法体中定义的局部变量

3.args_size 就比较好理解。它指的是方法的参数个数,因为每个方法都有一个隐藏参数 this,所以这里的数字是 2。

字节码执行过程

回顾一下 JVM 运行时的相关内容。main 线程会拥有两个主要的运行时区域:Java 虚拟机栈和程序计数器。其中,虚拟机栈中的每一项内容叫作栈帧,栈帧中包含四项内容:局部变量报表、操作数栈、动态链接和完成出口。
我们的字节码指令,就是靠操作这些数据结构运行的。下面我们看一下具体的字节码指令。


CgpOIF4ezeKAHVCXAABv7rzSgXE896.jpg
(1)0: aload_0

把第 1 个引用型局部变量推到操作数栈,这里的意思是把 this 装载到了操作数栈中。
对于 static 方法,aload_0 表示对方法的第一个参数的操作。


CgpOIF4w-GGAA6DnAAEtqWkdOnE696.jpg
(2)1: getfield #2

将栈顶的指定的对象的第 2 个实例域(Field)的值,压入栈顶。#2 就是指的我们的成员变量 a。


1.jpg
(3)i2l

将栈顶 int 类型的数据转化为 long 类型,这里就涉及我们的隐式类型转换了。图中的信息没有变动,不再详解介绍。

(4)lload_1

将第一个局部变量入栈。也就是我们的参数 num。这里的 l 表示 long,同样用于局部变量装载。你会看到这个位置的局部变量,一开始就已经有值了。


2.jpg
(5)ladd

把栈顶两个 long 型数值出栈后相加,并将结果入栈。


4.jpg
(6)getstatic #3

根据偏移获取静态属性的值,并把这个值 push 到操作数栈上。


3.jpg
(7)ladd

把栈顶两个 long 型数值出栈后相加,并将结果入栈。


5.jpg
(8)lstore_3

把栈顶 long 型数值存入第 4 个局部变量。还记得我们上面的图么?slot 为 4,索引为 3 的就是 ret 变量。


6.jpg
(9)lload_3

正好与上面相反。上面是变量存入,我们现在要做的,就是把这个变量 ret,压入虚拟机栈中。


7.jpg
(10)lreturn

从当前方法返回 long。
到此为止,我们的函数就完成了相加动作,执行成功了。JVM 为我们提供了非常丰富的字节码指令。详细的字节码指令列表,可以参考以下网址:

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html

注意点

注意上面的第 8 步,我们首先把变量存放到了变量报表,然后又拿出这个值,把它入栈。为什么会有这种多此一举的操作?原因就在于我们定义了 ret 变量。JVM 不知道后面还会不会用到这个变量,所以只好傻瓜式的顺序执行。

为了看到这些差异。大家可以把我们的程序稍微改动一下,直接返回这个值。

public long test(long num) {
       return this.a + num + C;
}

再次看下,对应的字节码指令是不是简单了很多?

0: aload_0
1: getfield     #2                 // Field a:I
4: i2l
5: lload_1
6: ladd
7: getstatic     #3                 // Field C:J
10: ladd
11: lreturn
----------------------------------那我们以后编写程序时,是不是要尽量少的定义成员变量?--------------------------------------

这是没有必要的。栈的操作复杂度是 O(1),对我们的程序性能几乎没有影响。平常的代码编写,还是以可读性作为首要任务。

你可能感兴趣的:(22. java虚拟机总结-从栈帧看字节码 (五))