引子:打开黑匣子 心中有数
老实说,对于C++的整个编译运行过程,我并没有全面的了解,好几次被问住了,看来是汇编没有学好,但是在看完《深入Java虚拟机》之后,对于Java代码到运行的每一个细节,有了更全面的认识。
描述一下整体的流程:程序员根据Java API编写Java程序,各种类文件,用一个Java编译器编译Java程序为class文件,class文件通过一定的分发方式被Java虚拟机装载、连接、初始化,变成Java虚拟机方法区的Class类,然后被实例化为堆区的对象,随着虚拟机的执行,对象可能不再被引用而被垃圾回收,随后class文件可能变得不可触及而被虚拟机卸载,最后虚拟机随着最后一个非后台的线程的结束而退出。
步骤一:编写Java程序【生成 *.java】
一个Java文件只允许定义一个public的Java类,因为虚拟机是通过文件名的机制搜索类型定义的。Java程序员根据Java API编写程序。
步骤二:编译Java文件为class文件【生成class】
class文件的结构包含了Java虚拟机所需要知道的关于类的所有信息。文件内容大致为:魔术、版本、常量池、this_class、super_class、接口、字段、方法、属性
class文件稍微有点绕的是常量池。常量池中的常量不是我们程序员角度理解的狭义的常量,一个类的名字是常量,一个方法的描述是常量,一个属性的描述也是常量。常量池中包含了所有的字面信息,通过索引,你可以查找到一个方法的名字和描述,一个字段的名字,一个常量字符串 等等。在连接模型中,有一个解析的过程,就是因为在字节码中,所有的都是符号引用,比如调用了某个方法,字节码是说调用了常量区的一项内容,这项内容表示的是一个方法,因此,这就需要进行解析,将这个常量区的引用转变为方法的直接引用。常量池中被解析的项都会被标记,这样下一次就不用再次解析了。
所以我们看class文件是如何完全表达一个类的信息的:类的名字通过this_class指向的常量池可以得到,父类通过super_class,接口、方法、字段 都可以通过相应的文件段在常量区中找到基本信息。方法的字节码在方法的属性字段中定义了字节码,方法的属性还定义了有多少个变量,需要用多大的栈空间。
步骤三:装载、连接、初始化【生成类】
在最开始我们强调通过一定的分发方式,这是因为class文件可能存在与运行的虚拟机上,也可能存在与网络上,通过用户自定义的类型装载器,开发人员可以定义灵活的机制获取代码。
1,装载。装载必须产生代表该类型的二进制流(读文件或者从网络或者其他方式),解析二进制数据为内部数据结构,创建一个 java.lang.Class类
2,验证。验证是否符合Java语言的语义。
3,准备。为类变量分配内存,并初始化为默认值。
4,解析。就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把他们替换为直接引用的过程。在符号引用被首次使用之前,解析都是可选的。
5,初始化。初始化为“正确”的初始值,即程序员期望的初始值,代码中static定义的,被收集到 <clinit> 方法中。
步骤四:类实例生成,垃圾回收【生成类实例:对象】
现在一切具备,程序开始运行。内存分别如下图
到此时,我想我们对整个流程有了一个全面的认识(我们刻意忽略了安全相关的话题)
从虚拟机的角度,最开始是面对的class文件,读入并解析class文件,将PC寄存器设置为main入口,然后开始执行字节码。字节码总是针对运算栈的,所以虚拟机知道操作数就在运算栈那里。字节码由操作符和操作数定义,操作数可能为Java栈的局部变量,可能为常量池的引用。如果是常量池的引用,并且还未解析,则需要进行解析。任何时候,栈中都是基本变量或者对象引用,按部就班执行就行了。