JVM--剖析类与对象在JVM中从生存至死亡

2021-05-15 10:29:35 Author: juejin.cn
觉得文章还不错?,点我收藏



0 引言

以这份 Java 代码为例,来剖析一个 Java 程序的生命历程:

interface ClassName {

    String getClassName();
}

class Company implements ClassName {

    String className;

    public Company(String className) {
        this.className = className;
    }

    @Override
    public String getClassName() {
       return className;
    }
}

public class Main {

    public static void main(String[] args) {
        String className;

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            className = scanner.next();
            Company company = new Company(className);
            System.out.println("name=" + company.getClassName());
        }
    }
}
复制代码

这份代码涉及到了接口,继承,对象的实例化,main 方法。值得我们花费一些功夫去从 JVM 层面上了解这个程序从编译、运行到结束都发生了哪些事情。

1 编译阶段

这段代码会生成 3 个 .class 文件。

.class 文件中保存了 魔数、版本符号、常量池、方法标志、类索引、父类索引、接口索引、字段表(有可能含有属性表)、方法表(有可能含有属性表) 等信息。我们可以通过字节码文件,清晰的描述出 Java 源码中有关类的所有信息。

在这里只以 Main 类为例,使用 javap 命令看一下生成的 .class 文件。

javap -verbose Main;
复制代码
  Last modified 2017-12-13; size 852 bytes
  MD5 checksum 0336fa14cc04a9c858c34cc016880c19
  Compiled from "Main.java"
public class Main
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #18.#29        // java/lang/Object."<init>":()V
   #2 = Class              #30            // java/util/Scanner
   #3 = Fieldref           #31.#32        // java/lang/System.in:Ljava/io/InputStream;
   #4 = Methodref          #2.#33         // java/util/Scanner."<init>":(Ljava/io/InputStream;)V
   #5 = Methodref          #2.#34         // java/util/Scanner.hasNext:()Z
   #6 = Methodref          #2.#35         // java/util/Scanner.next:()Ljava/lang/String;
   #7 = Class              #36            // Company
   #8 = Methodref          #7.#37         // Company."<init>":(Ljava/lang/String;)V
   #9 = Fieldref           #31.#38        // java/lang/System.out:Ljava/io/PrintStream;
  #10 = Class              #39            // java/lang/StringBuilder
  #11 = Methodref          #10.#29        // java/lang/StringBuilder."<init>":()V
  #12 = String             #40            // name=
  #13 = Methodref          #10.#41        // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #14 = Methodref          #7.#42         // Company.getClassName:()Ljava/lang/String;
  #15 = Methodref          #10.#43        // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #16 = Methodref          #44.#45        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #17 = Class              #46            // Main
  #18 = Class              #47            // java/lang/Object
  #19 = Utf8               <init>
  #20 = Utf8               ()V
  #21 = Utf8               Code
  #22 = Utf8               LineNumberTable
  #23 = Utf8               main
  #24 = Utf8               ([Ljava/lang/String;)V
  #25 = Utf8               StackMapTable
  #26 = Class              #30            // java/util/Scanner
  #27 = Utf8               SourceFile
  #28 = Utf8               Main.java
  #29 = NameAndType        #19:#20        // "<init>":()V
  #30 = Utf8               java/util/Scanner
  #31 = Class              #48            // java/lang/System
  #32 = NameAndType        #49:#50        // in:Ljava/io/InputStream;
  #33 = NameAndType        #19:#51        // "<init>":(Ljava/io/InputStream;)V
  #34 = NameAndType        #52:#53        // hasNext:()Z
  #35 = NameAndType        #54:#55        // next:()Ljava/lang/String;
  #36 = Utf8               Company
  #37 = NameAndType        #19:#56        // "<init>":(Ljava/lang/String;)V
  #38 = NameAndType        #57:#58        // out:Ljava/io/PrintStream;
  #39 = Utf8               java/lang/StringBuilder
  #40 = Utf8               name=
  #41 = NameAndType        #59:#60        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #42 = NameAndType        #61:#55        // getClassName:()Ljava/lang/String;
  #43 = NameAndType        #62:#55        // toString:()Ljava/lang/String;
  #44 = Class              #63            // java/io/PrintStream
  #45 = NameAndType        #64:#56        // println:(Ljava/lang/String;)V
  #46 = Utf8               Main
  #47 = Utf8               java/lang/Object
  #48 = Utf8               java/lang/System
  #49 = Utf8               in
  #50 = Utf8               Ljava/io/InputStream;
  #51 = Utf8               (Ljava/io/InputStream;)V
  #52 = Utf8               hasNext
  #53 = Utf8               ()Z
  #54 = Utf8               next
  #55 = Utf8               ()Ljava/lang/String;
  #56 = Utf8               (Ljava/lang/String;)V
  #57 = Utf8               out
  #58 = Utf8               Ljava/io/PrintStream;
  #59 = Utf8               append
  #60 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #61 = Utf8               getClassName
  #62 = Utf8               toString
  #63 = Utf8               java/io/PrintStream
  #64 = Utf8               println
{
  public 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 27: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=4, args_size=1
         0: new           #2                  // class java/util/Scanner
         3: dup
         4: getstatic     #3                  // Field java/lang/System.in:Ljava/io/InputStream;
         7: invokespecial #4                  // Method java/util/Scanner."<init>":(Ljava/io/InputStream;)V
        10: astore_2
        11: aload_2
        12: invokevirtual #5                  // Method java/util/Scanner.hasNext:()Z
        15: ifeq          63
        18: aload_2
        19: invokevirtual #6                  // Method java/util/Scanner.next:()Ljava/lang/String;
        22: astore_1
        23: new           #7                  // class Company
        26: dup
        27: aload_1
        28: invokespecial #8                  // Method Company."<init>":(Ljava/lang/String;)V
        31: astore_3
        32: getstatic     #9                  // Field java/lang/System.out:Ljava/io/PrintStream;
        35: new           #10                 // class java/lang/StringBuilder
        38: dup
        39: invokespecial #11                 // Method java/lang/StringBuilder."<init>":()V
        42: ldc           #12                 // String name=
        44: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        47: aload_3
        48: invokevirtual #14                 // Method Company.getClassName:()Ljava/lang/String;
        51: invokevirtual #13                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        54: invokevirtual #15                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        57: invokevirtual #16                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        60: goto          11
        63: return
      LineNumberTable:
        line 31: 0
        line 32: 11
        line 33: 18
        line 34: 23
        line 35: 32
        line 36: 60
        line 37: 63
      StackMapTable: number_of_entries = 2
        frame_type = 253 /* append */
          offset_delta = 11
          locals = [ top, class java/util/Scanner ]
        frame_type = 51 /* same */
}
SourceFile: "Main.java"
复制代码

直接从常量池开始分析一下上面的输出结果:

#1 = Methodref          #18.#29        // java/lang/Object."<init>":()V
复制代码

这是常量池里面的第一项数据,#1 代表索引,Methodref 告诉我们常量池中第一个索引表示的是一个方法引用,对这个方法引用的描述用常量池中第 18 项和第 29 项的内容可以进行描述,查一下常量池中第 18 项和第 29 项的内容,其实对应的就是后面注释的内容。它告诉了你这个方法所属的类是 Object,方法的简单名称是 <init>,方法的描述符是 ()V,也就是 Object 中的实例构造函数(对简单名称和描述符我在前面的博客中已经进行了说明)。

你也许想问,为什么 Main 类常量池中的第一项数据描述的是 Object 类中的无参构造函数?你可能忘了,所有类都应如此,Java 中所有的类都继承自 Object,常量池中也会保存他们父类的索引,因为在 Java 中,对象的初始化与实例化还有一条规则---先初始化与实例化父类,然后才是子类。

剩余的常量池分析与上面类似。

常量池下面的代码块,可以看到,一个 Main 类的默认构造函数,一个就是 main 方法了。关于这两个东西,等一下在字节码的执行阶段再说。

可以看到,.class 文件中,包含着详细的信息,有大量的信息都是你无法从源码中直接得到的。

2 类加载阶段

javac 对源文件编译完成,然后使用 java 命令开始运行这个 Main 类。java命令只能运行包含 main 方法的类

java 命令一开始运行,JVM 开始对 Main 类进行加载

JVM 在加载这个 Main 类的时候,使用类加载器(双亲委派模型)对其进行加载,经历了加载、验证阶段,由于在 Main 类中并没有类变量,也就相当于跳过了准备这一阶段,然后对字节码进行解析,由于 main 方法是静态方法,也就是非虚方法,开始静态链接,在字节码中直接将 main 方法的符号引用解析为直接引用。由于没有静态变量与静态语句块,所以初始化这一阶段也相当于是直接跳过,最后整个加载过程完毕,并在方法区中生成 Main 类所对应的 Class 对象

由于这个例子中不涉及多态,也就不涉及分派,但这部分知识请务必掌握。

3 方法执行阶段

类中所有的信息已经在内存中加载完毕,JVM 开始进行方法调用

方法调用:JVM 开始执行 main 方法,这部分工作是由虚拟机中字节码执行引擎完成的。main 方法会由一个线程进行调用。此线程会在虚拟机栈上为自己开辟一部分的栈空间,此后只要这个线程调用新的方法,这个方法便会被当作栈帧压入虚拟机栈的栈顶(作为当前栈帧)。这个方法中定义的局部变量会被存储进局部变量表,在JVM中,并不存储局部变量的名称,他们都是以局部变量表的相对偏移量来标识每个不同的局部变量

我以 main 方法的 Code 属性再说明一下栈帧中的局部变量表以及操作数栈(.class 文件中方法表的Code 属性保存的是 Java 方法体中的字节码)。

Code:
stack=3, locals=4, args_size=1
复制代码

可以看到,main 方法在调用之前(实际上在编译阶段,它的局部变量表,操作数栈的大小都已确定),locals 为 4,也就是局部变量表的大小为 4:this,className,scanner,company;stack 为 3,也就是操作数栈的大小 3:className,scanner,company。

我们还可以在上面方法体的字节码当中看到许多指令,而这些指令就是方法在执行的过程中,JVM 需要解释运行的,如下:

0: new           #2                  // class java/util/Scanner
复制代码

前面的 0 表示的也是相对偏移量,而 new 指令就是新建一个对象,对应 Java 源码中的 new,后面的 #2 代表的是 new 指令的参数,表达的意思是常量池中索引为 2 的数据项,也就是上述代码后面注释中的 Scanner 类。

值得一提的是,这里的 Scanner 由于是对类的实例化,因此 JVM 会首先判断 Scanner 这个类是否已经被加载进内存,如果没有被加载进内存,JVM 开始对这个类进行类加载,过程如上述步骤,然后进行对象的初始化。

指令一条条的向下执行,最终执行到这一步:Company company = new Company(className);。也是一个类的实例化,但它和上面的 Scanner 又有点不同,这个类不仅实现了 ClassName 接口,而且实例化的时候还进行了传参。那么何时会加载 ClassName 接口呢?我根据查阅的资料,猜测是在 Company 类加载中的验证阶段会触发其接口的加载,具体大家可以 Google、baidu。

那么如何实现参数的传递呢?在实例化 Company 类的过程中,执行了 Company 的构造方法,而构造方法本身又是一个方法,因此可以理解为实参先进入被调用方法的操作数栈中,然后再出栈赋值给被调用方法的局部变量表。

最后,代码执行完毕,程序退出。

4 参考阅读

  1. JVM 内部原理(六)— Java 字节码基础之一



觉得文章还不错?,点我收藏



如果文章侵犯到您的版权,请联系我:buaq.net[#]pm.me