在 Jvm 内存模型之初探花裙中我们已经大致了解了jvm内存的逻辑结构以及各个存储区域的作用。然而在迈向资深老司机的道路上,浅尝辄止可不行,撩起花裙加深印象才符合老司机的风格。接下来咱们可以一个java类文件来研究类执行过程中其各个部分如何与jvm内存逻辑模型的各个部分映射的。
public class HelloWorld implements Runnable {
private String[] aHeapStringArray = {"a"+"a"};
private static String aStaticString = "aa";
private final String aFinalString = "aa";
static {
System.out.println("hello world on load");
}
public void aVoidMethod(){
int aInt = 888;
System.out.println("hello world thread end");
}
@Override
public void run() {
aVoidMethod();
}
public static void main(String[] agr) throws InterruptedException {
int aInt = 888;
Thread thread = new Thread(new HelloWorld());
thread.start();
Thread.sleep(aInt);
System.out.println("main thread end");
}
}
这个类实现了Runnable 接口,共包含三个属性、三个方法、一个静态代码块。那么就从作为入口的main方法开始逐步分析。
第一步:在执行main方法之前,需要先载入HelloWorld的class文件
- jvm会先去方法区查找是否存在HelloWorld类信息。
- 当jvm未查找到HelloWorld类的相关信息时会去classpath下找到HelloWorld的class文件并装载,将class文件的字节流结构化到方法区的存储空间中。
- 装载完成后,在链接时将class包含字面量”a”,”aa”,888,”hello world on load”,”hello world thread end”,”main thread end” 并将这些字面量存入常量池。可以注意到,程序中出现了多处”a”,”aa”,888等字面量,但是在常量池中每个字面量仅会存储一份。可简单理解为常量不重复。然后将代码中的字面量替换为引用。
- 链接完成后,类初始化过程开始,并执行class包含静态代码。调用系统函数输出”hello world on load”。至此class载入及初始化过程完成。
第二步:jvm调用main方法
- jvm启动一个线程调用HelloWorld,即执行主线程。
- 根据main方法中的局部变量,例如int aInt 、Thread thread、返回值信息等生成栈帧并将栈帧压入虚拟机栈。
- 修改主线程的程序计数器数值,记录当前执行的字节码代码行号。(伴随着代码执行同步修改的,下文中不再说明)
- 调用Thread的构造方法,生成该方法的栈帧压栈。
- 调用HelloWorld的构造方法(由于HelloWorld没有构造方法,此处调用默认构造方法),生成此构造方法的栈帧并压入虚拟机栈(为了简化理解此处忽略了调用父类的构造方法过程,姑且任务此时处于栈顶)。
- 向堆内存申请一块内存区域存储HelloWorld的实例,实例中至少包含对于其他对象的引用(例如数组引用),类信息等。
- 执行完HelloWorld的构造方法后返回HelloWorld对象实例的引用, HelloWorld的栈构造方法的栈帧弹栈,并继续执行当前栈顶的Thread的构造方法。
- Thread构造方法执行完毕,将此线程的对象存放在Thread thread的引用中。
- 将Thread的start方法入栈,执行start方法,启动hello world 线程。主线程程序计数器记录当前主线程代码执行位置。
- 若cpu切换至hello world 线程时,新增一个程序计数器,新增虚拟机栈,将run方法压栈,开始执行run方法。
- 若cpu切换回主线程,则主线程根据程序计数器记录的位置继续执行。
- 当主线程虚拟机栈的方法全部弹出时主线程执行完毕,hello world 线程同理。至此,方法执行完毕。
其实jvm执行类的细节远比上文描述中复杂,本文的目的仅仅是梳理类执行的过程与jvm内存模型中的各个部分关系。若需详细了解其执行细节,还需要进一步梳理。
小弟原创,欢迎拍砖