- 一.概述
- 二.执行细节
- 三.总结
一.概述
程序执行流程我把它划分为以下几个步骤:编辑源码、编译生成class文件、(加载class文件、运行class字节码文件),其中后两个步骤都是在jvm虚拟机上执行的。
二.执行细节
2.1 一个Java类
package com.javase.string;
class Person {
private String name;
private int age;
public Person(int age, String name) {
this.age = age;
this.name = name;
}
public void run() {
}
}
interface IStudyable
{
public int study(int a, int b);
}
// public类,与java文件同名
public class Student extends Person implements IStudyable
{
private static int cnt = 5;
static {
cnt++;
}
private String sid;
public Student(int age, String name, String sid) {
super(age, name);
this.sid = sid;
}
public void run() {
System.out.println("run()...");
}
public int study(int a, int b) {
int c = 10;
int d = 20;
return a + b * c - d;
}
public static int getCnt() {
return cnt;
}
public static void main(String[] args) {
Student s = new Student(23, "dqrcsc", "20150723");
s.study(5, 6);
Student.getCnt();
s.run();
}
}
2.2 编译
生成.class字节码文件,输入命令javac Student.java将该源码文件编译生成.class字节码文件。由于在源码文件中定义了两个类,一个接口,所以生成了3个.clsss文件。
这部分的操作就相当于我们在myeclipse这样的ide上写完代码ctrl+s保存
2.3 运行
在命令行中输入java Student这个命令,就启动了一个java虚拟机,然后加载Student.class字节码文件到内存,然后运行内存中的字节码指令了。
这部分的操作就相当于我们在myeclipse这样的ide上点击运行按钮
- 运行阶段主要包括class文件加载,执行class 文件,垃圾回收。我们这里主要讲 前面一二阶段。
- 2.3.1 class 文件加载
- 1.加载阶段
1)类加载器会在指定的classpath中找到Student.class(通过类的全限定名)这个文件,然后读取字节流中的数据,将其存储在方法区中。
2)会根据Student.class的信息建立一个Class对象,这个对象比较特殊,一般也存放在方法区中,用于作为运行时访问Student类的各种数据的接口。
- 2.验证阶段:
3)必要的验证工作,格式、语义等 。
- 3.准备阶段:
4)为Student中的静态字段分配内存空间,也是在方法区中,并进行零初始化,即数字类型初始化为0,boolean初始化为false,引用类型初始化为null等。
此时,并不会执行赋值为5的操作,而是将其初始化为0.
- 4.解析阶段
5)由于已经加载到内存了,所以原来字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用了,而不一定非要等到真正运行时才进行解析.
- 5.初始化阶段
6)由于已经加载到内存了,所以原来字节码文件中存放的部分方法、字段等的符号引用可以解析为其在内存中的直接引用了,而不一定非要等到真正运行时才进行解析。
2.3.2 class 文件执行
执行引擎找到main()这个入口方法,执行其中的字节码指令:
只有当前正在运行的方法的栈帧位于栈顶,当前方法返回,则当前方法对应的栈帧出栈,当前方法的调用者的栈帧变为栈顶;当前方法的方法体中若是调用了其他方法,则为被调用的方法创建栈帧,并将其压入栈顶。
简单查看Student.main()的运行过程:
public static void main(String[] args) {
Student s = new Student(23, "dqrcsc", "20150723");
s.study(5, 6);
Student.getCnt();
s.run();
}
Mximum stack depth:指定当前方法即main()方法对应栈帧中的操作数栈的最大深度,当前值为5
Maximum local variables:指定main()方法中局部变量表的大小,当前为2,及有两个slot用于存放方法的参数及局部变量。
Code length:指定main()方法中代码的长度。
执行详细过程如下:
- 1.为main方法创建栈帧:
局部变量表长度为2,slot0存放参数args,slot1存放局部变量Student s,操作数栈最大深度为5。
- 2.new#7指令,在java堆中创建一个Student对象,并将其引用值放入栈顶
- 3.初始化一个对象(通过实例构造的方式)
up指令:复制栈顶的值,然后将复制的结果入栈。bipush 23:将单字节常量值23入栈。ldc #8:将#8这个常量池中的常量即”dqrcsc”取出,并入栈。ldc #9:将#9这个常量池中的常量即”20150723”取出,并入栈。
- 4.invokespecial #10:调用#10这个常量所代表的方法,即Student.()这个方法,
这步是为了初始化对象s的各项值。<init>()
方法,是编译器将调用父类的<init>()
的语句、构造代码块、实例字段赋值语句,以及自己编写的构造方法中的语句整合在一起生成的一个方法。保证调用父类的<init>()
方法在最开头,自己编写的构造方法语句在最后,而构造代码块及实例字段赋值语句按出现的顺序按序整合到<init>()
方法中。注意到Student.()方法的最大操作数栈深度为3,局部变量表大小为4。
此时需注意:从dup到ldc #9这四条指令向栈中添加了4个数据,而Student.()方法刚好也需要4个参数:
public Student(int age, String name, String sid) {
super(age, name);
this.sid = sid;
}
虽然定义中只显式地定义了传入3个参数,而实际上会隐含传入一个当前对象的引用作为第一个参数,所以四个参数依次为this,age,name,sid。上面的4条指令刚好把这四个参数的值依次入栈,进行参数传递,然后调用了Student.()方法,会创建该方法的栈帧,并入栈。栈帧中的局部变量表的第0到4个slot分别保存着入栈的那四个参数值。创建Studet.()方法的栈帧:
Student.()方法中的字节码指令:
aload_0:将局部变量表slot0处的引用值入栈aload_1:将局部变量表slot1处的int值入栈aload_2:将局部变量表slot2处的引用值入栈nvokespecial #1:调用Person.()方法,同调用Student.过程类似,创建栈帧,将三个参数的值存放到局部变量表等,这里就不画图了……
从Person.()返回之后,用于传参的栈顶的3个值被回收了。aload_0:将slot0处的引用值入栈。aload_3:将slot3处的引用值入栈。putfield #2:将当前栈顶的值”20150723”赋值给0x2222所引用对象的sid字段,然后栈中的两个值出栈。
return:返回调用方即main()方法,当前方法栈帧出栈。重新回到main()方法中,继续执行下面的字节码指令:astore_1:将当前栈顶引用类型的值赋值给slot1处的局部变量,然后出栈。
- 5,到这儿为止,第一行代码执行完毕,将s返回给局部变量表,执行下边的
- 后面略…
三.总结
- 总结起来,一个类文件首先加载到方法区,一些符号引用被解析(静态解析)为直接引用或者等到运行时分派(动态绑定),经过一系列的加载过程(class文件的常量池被加载到方法区的运行时常量池,各种其它的静态存储结构被加载为方法区运行时数据解构等等)
- 然后程序通过Class对象来访问方法区里的各种类型数据,当加载完之后,程序发现了main方法,也就是程序入口,那么程序就在栈里创建了一个栈帧,逐行读取方法里的代码所转换为的指令,而这些指令大多已经被解析为直接引用了,那么程序通过持有这些直接引用使用指令去方法区中寻找变量对应的字面量来进行方法操作。
- 操作完成后方法返回给调用方,该栈帧出栈。内存空间被GC回收,堆里被new的那些也就被来及回收机制GC了。
- 全流程包括以下几步:源码编写–编译(javac编译和jit编译,java语法糖)—类文件被加载到虚拟机(类Class文件结构,虚拟机运行时内存分析,类加载机制)—-虚拟机执行二进制字节码(虚拟机字节码执行系统)—垃圾回收(JVM垃圾回收机制)