接上一篇 一个java程序是怎样运行起来的(2),在jvm创建好后,就可以开始执行程序了。我们知道,程序执行的入口在main函数,所以我们首先得找到main函数,这得有个前提,main函数对应的类已经被jvm加载了,所以jvm做的第一件事就是去加载类。先来看下java类加载的机制,主要有以下几个阶段:
1,加载:
加载阶段可以参考java.lang.ClassLoader中loadClass方法,采用的是双亲委托进制进行加载,这个阶段首先找到对应的class文件,以二级制方式读入内存,按照jvm规范解析出所表达的数据结构,在内存中生成一个代表该类的java.lang.Class对象.
2,验证:
验证是确保当前class文件格式符合jvm规范,不会对jvm产生危害。验证工作并不是在加载之后才开始的,比如从class文件读入到内存后,解析其代表的数据结构时,我们首先会去校验魔数是否正确,以及版本号是否符合要求等
3,准备
准备阶段主要是为类的静态变量分配内存,设定初始值等工作
4,解析
常量池中的符号引用替换为直接引用,比如String str = "test",str指向常量池中"test"的地址
5,初始化
这个过程主要是执行类构造器的方法,静态类的赋值,静态代码块的执行。如果初始化一个类时,发现父类还没有初始化,则需要先初始化父类
根据一个java程序是怎样运行起来的(1),类加载完成后,就可以找到main方法了,这时就可以开始执行main方法中的jvm指令了,下面以一个例子来解释其执行过程。
测试代码如下:
public class TestAdd{
public static void main(String[] args){
int a = 1;
int b = 2;
int c = a+b;
print(c);
}
public static void print(int c){
System.out.println(c);
}
}
javac编译后,利用命令javap -c TestAdd,我们来看下在运行时究竟执行了哪些jvm指令
public class TestAdd {
public TestAdd();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: istore_1
2: iconst_2
3: istore_2
4: iload_1
5: iload_2
6: iadd
7: istore_3
8: iload_3
9: invokestatic #2 // Method print:(I)V
12: return
public static void print(int);
Code:
0: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
3: iload_0
4: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
7: return
}
构造函数先不看,直接看main方法。函数的执行是在栈帧中执行的,执行的时候由程序计数器记录当前执行到哪个位置。栈帧存放在stack中,只有stack顶的栈帧当前有效,里面存放了本地变量表,操作数栈,返回地址等,本地变量表的大小,以及从操作数栈的深度在编译时就已经确定,运行时不会改变,如下图:
他们之间的调用关系是栈帧1的函数调用了栈帧2中的函数,栈帧2中国的函数调用了栈帧3中的函数。有了这个基础,接下来看下上面的指令是如何执行的,入口在main方法,此时分配新的栈帧我把它标记为栈帧1,栈帧1处在stack顶,即为当前栈帧,执行main方法中jvm指令之前,栈帧1中的有关数据结构如下,本地变量表index为0变量存放的是函数的参数args:
执行指令iconst_1,将int类型数字1push到操作数栈,此时栈帧1的数据结构为:
执行指令istore_1,将栈帧1中操作数栈执行退栈操作,所得的值放入到本地变量表第1个变量中,此时栈帧1的数据结构:
iconst_2,istore_2与上面同理,执行后栈帧:
iload_1和iload_2分别把本地变量表中第一个和第二个变量的值压入到操作数栈,
iadd指令,连续两次操作数栈执行退栈操作,将所得的值相加得到结果3再次压入操作数栈
istore_3,将操作数栈栈顶元素退栈,所得的值存入第3个本地变量中
iload_3,将本地变量表中第三个变量的值压入操作数栈中
invokestaic 调用静态方法,此处存在方法调用,需要新开辟一个栈帧压入stack,并把变量值3入本地变量表,其返回地址为main方法中即将执行的指令return的地址,这个暂按照指令集的排列,标记为frame1_12吧,此时当前栈帧为栈帧2,运行时数据结构为:
接下来看下print方法的执行,
getstatic获取指定的field,
iload_0,将变量0的值3压入操作数栈,此时数据结构为:
invokevirtual执行打印方法,会新开辟一个栈帧栈帧3,将变量值3入栈帧3的本地变量表,其返回地址为frame2_7,执行return后,当前栈帧栈帧3退栈,栈帧2变成当前栈帧,发现当前执行的指令为return执行退栈操作,当前栈帧为栈帧1,此时栈帧1要执行的指令为return,退栈,至程序结束退出。
至此,把java程序的执行过程简单过了一遍,过程非常粗糙,我目前对jvm的了解有限,后续有更好更深入的理解后,再回过头来丰富下。
上面方法调用时的栈帧等数据结构可以参见本人编写的jvm尝试: https://github.com/reverence/czvm