点击 Run 按钮时,JVM 做了什么?

考虑如下 Java 代码——

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Student.java 文件
public class Student {
public String name;
public Student(String name) {
this.name = name;
}
public void sayName() {
System.out.printf("my name is %s\n", name);
}
}

// Main.java 文件
public class Main {
public static void main(String[] args) {
String stud = new Student("arisa");
stud.sayName();
}
}

当执行 Main 类的 main 方法时(这里就假设我们在使用 IDEA,点击了 main 前面的:arrow_forward: 按钮吧!)

  1. 首先,调用 javac,将 Main.java 编译成 java 字节码文件,即 Main.class。
  2. 使用 java 执行 Main.class 文件。这时,一个 JVM 进程被启动,它通过 classpath 路径找到 Main.class 文件,使用类加载器(ClassLoader)将类信息加载到运行时数据区的方法区内。然后,在堆中生成指向类信息的Class对象。这是 Java 的类加载过程(这里得提出一个问题,Class 对象中究竟存储了什么?不会只有一个指向类信息的指针和一堆偏移量吧?)。然后是链接和初始化。
  3. JVM 找到 Main 的主程序入口(应该是通过 Class 对象找到的),执行 main 方法。
  4. main 方法的第一句为String stud = new Student("arisa"),它要求 JVM 创建一个 Student 对象的实例。JVM 将首先检查 Student 类是否被加载(如果没有加载,则方法区中不存在 Student 类的信息,因此无法创建实例)。这里显然是未被加载的。因此,将会进行类的加载过程。
    进行处理的首先是AppClassLoader。(当然,在这之前总得将 Student.java 编译并弄到内存里)它将委托它的父类加载器ExtClassLoader进行加载,ExtClassLoader再委托它的父类加载器BootstrapClassLoader进行加载。BootstrapClassLoader没有父类加载器,因而它将尝试加载,加载失败后,它将返回,由ExtClassLoader进行加载,其也将加载失败,由AppClassLoader进行加载。这就是类加载器的所谓双亲委派机制。Main.class 也是如此加载的。
  5. 类加载后,JVM 在中分配一个新的 Student 实例的内存。然后调用构造函数初始化 Student 的实例。每个 Student 的实例都持有对方法区中 Student 类信息的引用。之后,将该实例的地址赋给 stud。这里的 stud 称为对象引用,它指向的堆中实际的对象称为对象实例。stud 存储在栈中,而它指向的对象实例存在于堆中。
  6. 然后它准备执行 stud.sayName()。JVM 根据 stud 这个引用找到在堆中的 Student 对象实例,然后根据 Student 对象持有的引用定位到方法区中 Student 的类信息中的方法表,获得 sayName() 的字节码地址。
  7. 将 stud 作为参数(应该……如此),执行 sayName() 方法。(就像 python 中定义实例方法需要显式指明 self 一样,实例方法其实是需要对象实例作为参数的,因为实例方法其实也是只存在一个的,就如静态方法一样,它无法保存类的实例的地址在方法里,所以类的实例要传递给实例方法)。

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!