一个java程序是怎样运行起来的(3)

来源:互联网 发布:雷达回波数据 编辑:程序博客网 时间:2024/06/04 17:57

接上一篇 一个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."<init>":()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



原创粉丝点击