最近在学习JVM和Java高级特性,有了一些感悟,在此总结,如果有不对的地方,希望大家指出。
假设我们编写了一个java类,代码如下。
public class Student { static final String schoolName = "中南林业科技大学"; public String name; private Student(String name) { this.name = name; } public void speak() { System.out.println("我叫" + name + ",就读于" + schoolName); } public static void main(String[] args) { String name = new String("黄超"); Student student = new Student(name); student.speak(); } }
编写完成后,打开项目文件夹,我们可以看到一个名为 student.java 的文件。这就是我们编写的源文件。
现在我们执行打开 cmd ,执行 javac 命令,将它编译为一个字节码文件,这样就获得了一个名为 student.class 的文件。
接下来,我们开始执行这个类。
当我们输入执行命令后,Java虚拟机首先会加载这个类,为此JVM会自顶向下选用适当的类加载器。由于这个类是我们自己编写的,最后JVM会使用application加载器。
在加载的过程中,JVM按照类名,会读取磁盘上的 Student.class ,它是一个二进制字节流(Class字节码)。JvM会将读取的内容构建为一个class对象,也就是Student这个类的模板,然后将它放入方法区。
方法区在不同的jdk版本中,处于内存的不同的位置,对于jdk8而言,它位于Java堆的元空间中,之前这个位置被称为永生区。
加载完成后,还有链接和初始化两个步骤。在链接阶段JVM将验证Class文件中的字节流包含的信息是否符合当前虚拟机的要求。同时,会将静态变量 schoolName 放入方法区的常量池。在初始化阶段,JVM会执行模板类中的静态代码块,由于我们这个类没有,所以不会执行。
完成这一切之后,就开始正式执行了。
首先是执行主函数,JVM会将主函数放入Java虚拟机栈的栈底,为它分配一个栈帧。栈帧中存放了它的方法名、参数、运行时的临时变量。同时还有一个指向方法区中函数实体的引用。
然后就是逐行执行函数。
当函数执行到第三行时,在这里我们用 new 关键字,创建了一个 String 类型的对象 name 。
这时,JVM首先会实例化一个 String 对象放入堆空间的常量池,它的值为 “黄超” 。
然后虚拟机会在 main 的栈帧中选择一个页面,存放一个 String 类型的引用。
同时在堆中开辟一片区域,用于存放这个对象的实例。
对象的实例会首先被放在伊甸园区,经历一轮GC它后进入幸存区,经历15轮GC后会进入老年区。但这都是后话,在我们这个程序中,它因该是不会进入老年区的。
现在我们来分析以下对象实例在堆中伊甸园区是怎么组织的。它包含三个部分对象头、实例数据和对齐填充。
对象头主要包括两类信息:第一类被称为Mark Word,是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态、线程持有的锁、偏向线程ID、偏向时间戳等。第二类是类型指针,用于确定对象是哪个类的实例,即指向方法区的类模板。
实例数据可以认为就是“黄超”这个字符串本身。
对其填充则是为了起占位作用,HotSpot 虚拟机要求对象的起始地址必须是8的整数倍,因此需要对齐填充。
将对象实例放入堆后,JVM会将栈中的对象引用指向堆中的实例。
随后继续执行下一行。
在这里,我们创建了一个 Student 对象,与之前创建 String 对象的过程大致相同。但要注意,这一次,堆空间中 name 存放的是一个引用,而不是一个数据。这个引用指向的正是我们之前创建的那个字符串对象,也就是作为构造函数参数传入的 name 对象。
继续, main 将会调用 student 对象的 speak 方法。它通过对象的引用,访问到堆中的对象实例,然后通过对象实例中的类型信息找到它的模板类,也就是在最开始,JVM放入方法区的那个Class类。最后它会在方法区中找到 speak 方法。
JVM随后就会将找到的这个方法压栈。
执行 speak 方法,会向控制台打印一串字符串,然后结束,退栈。之后 main 方法也会结束,退栈。这会导致Java虚拟机栈变为空,即程序执行完毕。
但是我们先不着急执行完程序,在它打印之前,也就是 speak 方法刚完成入栈的那一刻,我们来看看JVM的内存是什么情况。
虽然有些杂乱,但这正和我们之前分析的整个程序执行过程的一样。
最后,speak 函数执行,控制台打印出一串字符串。随后结束程序。
看完这里,一个类的旅程也就差不多结束了。