大家也不用找四、五、六了,都在这一起写了,因为这几节单独写个博客感觉没意思,在我看完《自己动手写Java虚拟机》第七章并且实现之后,才能把这前几个章节给串起来。
文字比较枯燥,所以我还是给大家分析个例子吧,比如,下面这个类的执行流程。
public class jj {
public static void main(String[] args) {
System.out.println(add(2));
}
public static long add(long n) {
return n + 1;
}
}
当我们在IDEA中执行这个java程序的时候,IDEA会帮我们把这个java文件编译成class文件,然后调用java去执行class文件。
执行的过程大概是这样的:
一、类加载器加载Class文件
这里所说的加载,也就是指类的加载机制,包括有加载——验证——准备——解析——初始化这几个步骤。
加载、 验证、 准备、 初始化和卸载这 5 个阶段的顺序足确定的, 类的加栽过程必须按照这种顺序按部就班地开始, 而解析阶段则不一定 : 它在某些情况下可以在初始化阶段之后冉开始, 这是为了支持 Java 语言的运行时绑定( 也称为动态绑定或晚期绑定).注意, 这里笔者写的是按部就班地‘‘ 开始”, 而不是按部就班地“ 进行” 或“ 完成”, 强调这点是因为这些阶段通常都是互相交叉地混合式进行的, 通常会在一个阶段执行的过程中调用、激活另外一个阶段。——引用自《深入理解Java虚拟机》
二、类方法调用与返回
在执行完Class文件加载后,这个类在内存中就会有一个Class对象代表这个类,通常这个存放Class对象信息的内存区域,叫做方法区。
之后,会调用这个类信息中的Main主方法。
怎么调用呢?首先我们知道,在内存中,方法的所有代码都会被编译成字节码信息,如下图:

执行完这些字节码,方法也就执行完了。
那么,这些字节码指令是怎么执行的呢?
在讲解之前,要跟大家介绍下JVM运行时内存区域的划分。
运行时内存可以分为两大类,一类是线程共享内存,一类是非线程共享的内存。多线程共享的运行时数据区需要在Java虚拟机启动时创建好,在Java虚拟机退出时销毁。线程私有的运行时数据区则在创建线程时才创建,线程退出时销毁。
多线程共享的内存区域主要存放两类数据:类数据和类实例(也就是对象)。对象数据存放在堆中,类数据存放在方法区中。
线程私有的运行时数据区用于辅助执行Java字节码。每个线程都有自己的pc寄存器和Java虚拟机栈。Java虚拟机栈又由栈帧(后面简称帧)构成,帧中保存方法执行的状态,包括局部变量表和操作数栈等。
每一个方法,都是一个帧,帧中的局部变量表和操作数栈,大家可以理解为局部变量表就是一个数组,操作数栈就一个栈结构,局部变量表和操作数栈的大小,是在编译成Class的时候确定的,在加载类的时候,进行了大小定义,在这里我们可以看到,main方法的局部变量表大小为1,操作数栈大小为3

在执行java字节码的时候,会有一个字节码解释执行引擎,也可以简单理解为一个for循环,循环执行字节码。
比如第一条指令是getstatic,这个指令获取指定类的静态域,并将其值压入栈顶,执行完之后,操作数栈深度为1,局部变量表大小为0。Slot大家可以理解为操作数栈和局部变量表的最小存储单元,可以存放一个int大小的整数或者一个Object引用
LocalVars(slots=[Slot(num=0, ref=null)])
OperandStack(size=1, slots=[Slot(num=0, ref=null), Slot(num=0, ref=null), Slot(num=0, ref=null)])
接着是第二条指令ldc2_w,将long或double型常量值从常量池中推送至栈顶(宽索引),执行完之后,操作数栈深度为3,局部变量表大小为0。为什么是3呢?因为这里是宽索引,占了两个单位
LocalVars(slots=[Slot(num=0, ref=null)])
OperandStack(size=3, slots=[Slot(num=0, ref=null), Slot(num=2, ref=null), Slot(num=0, ref=null)])
第三条指令invokestatic,调用静态方法add(),调用之后,会读取add方法所需参数大小,然后从操作数栈中弹出对应大小的参数,并且创建新的桢,压入线程中,再将弹出的参数,存放在add方法的桢里的局部变量表里,以此达成方法调用以及参数传递。
LocalVars(slots=[Slot(num=0, ref=null)])
OperandStack(size=1, slots=[Slot(num=0, ref=null), Slot(num=2, ref=null), Slot(num=0, ref=null)])
接着是进入add方法:

第一个指令是lload_0, 将第一个long型本地变量推送至栈顶,所以执行后这个桢的操作数栈和局部方法表如下(注意:每个方法所代表的桢的操作数栈和局部方法表都是独立的)
LocalVars(slots=[Slot(num=2, ref=null), Slot(num=0, ref=null)])
OperandStack(size=2, slots=[Slot(num=2, ref=null), Slot(num=0, ref=null), Slot(num=0, ref=null), Slot(num=0, ref=null)])
接着是lconst_1,将long型1推送至栈顶
LocalVars(slots=[Slot(num=2, ref=null), Slot(num=0, ref=null)])
OperandStack(size=4, slots=[Slot(num=2, ref=null), Slot(num=0, ref=null), Slot(num=1, ref=null), Slot(num=0, ref=null)])
然后是ladd,将栈顶两long型数值相加并将结果压入栈顶
LocalVars(slots=[Slot(num=2, ref=null), Slot(num=0, ref=null)])
OperandStack(size=2, slots=[Slot(num=3, ref=null), Slot(num=0, ref=null), Slot(num=1, ref=null), Slot(num=0, ref=null)])
最后是lreturn,从当前方法返回long,在这个指令的实现中,会弹出当前帧,也就是add方法,那么下次执行的就是main方法了,并且会将当前帧的操作数栈,弹出长度为Long的Slot(因为是lreturn,同理还有ireturn、freturn等等),再将弹出的值压入将要执行的桢中的操作数栈中。这也就是方法返回。
LocalVars(slots=[Slot(num=3, ref=null), Slot(num=0, ref=null)])
OperandStack(size=0, slots=[Slot(num=3, ref=null), Slot(num=0, ref=null), Slot(num=1, ref=null), Slot(num=0, ref=null)])
此时回到main方法,接着执行下一指令Invokevirtual,调用实例方法System.out.println()输出操作数栈中的值。
最后是return指令,从当前方法返回void。
到此,这个java程序就执行完毕了,也给大家简单介绍了方法的调用和返回。
写的不好的地方,大家可以在下方评论,也可以去clone我的项目https://github.com/ZheBigFish/myjvm
本文详细解析了Java程序从源代码到执行的全过程,包括类加载机制、字节码执行及方法调用与返回的原理,通过具体示例加深理解。
796

被折叠的 条评论
为什么被折叠?



