在Java种存在以上多种类加载器,它们之间通过一定的规则相互配合,这个规则就是双亲委派模型:每个类加载器收到类加载请求时,都会先将请求委派给父类加载器去完成,所以,加载请求会一直传递到最顶层的类加载器。只有当类父类加载器无法完成加载请求的时候,该加载器才会自己去加载。当然,这不是强制的,我们也可以完全使用自己的一套逻辑。但双清委派模型的好处就在于,假如你自定义了一个类java.lang.Object,那么当使用双亲委派模型来加载的时候,会由子加载器不断向上传递加载请求到启动类加载器进行加载,因此Object在各种类加载环境中都是同一个类。这保障了Java系统的稳定性。 3、虚拟机字节码执行引擎虚拟机是相对于物理机而言的,它们的区别是物理机的执行引擎直接建立在处理器、硬件、指令集和操作系统层面上,而虚拟机的执行引擎是自己实现的。所以,执行引擎也是Java虚拟机最核心的组成部分之一。
执行引擎用来执行我们写的业务逻辑,而业务逻辑就是指一些方法,所以虚拟机执行引擎就是用来执行各个方法的。而方法是通过栈帧来描述的,方法的执行是用栈帧入栈和出栈来描述的,栈帧中存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。所以说,执行引擎就是用来执行各个栈帧的。在虚拟机执行的时候,只有最顶层的栈帧是有效的,与之关联的方法就称为当前方法,并且执行引擎运行的所有字节码都是针对当前栈帧的。
方法的信息存储在栈帧中,而栈帧中的方法信息是从Class文件中读取来的。回到之前的Class类文件结构部分,每个方法是通过结构体method_info来描述的:
struct method_info{ u2 access_flags; //方法修饰符掩码 u2 name_index; //方法名在常数表内的索引 u2 descriptor_index; //方法描述符,其值是常数表内的索引 u2 attributes_count; //方法的属性个数 attribute_info **attributes; //方法的属性数据,};复制代码在method_info中又包含了一个属性表集合attribute_info类型的attributes,方法的局部变量表需要的空间大小和操作栈的深度等就记录在其中。局部变量表用于存放方法参数和方法内的局部变量,当方法是实例方法的时候,局部变量表的第0位会被用来传递方法所属对象的引用,即this。Java虚拟机执行引擎是基于栈的执行引擎,这里的栈就是操作数栈。操作数栈的深度也是记录在方法属性集合的Code属性中的。 执行引擎的执行过程我们可以用下面的一个程序来说明以下在实际的执行过程中,Java执行引擎是如何工作的。
public int cal() { int a = 100; int b = 200; int c = 300; return (a + b) * c; // 1}复制代码首先会在编译期确定方法的栈深度和局部变量表的长度,栈深度是由计算的过程得到的,而局部变量表的长度等于1个this加3个局部变量即4。当程序执行到1处时,局部变量表内会被填充为this、100、 200和300。程序计数器会随着代码当前执行到的位置而不断更新。而此时因为没有进行任何计算,所以栈还是空的。
接下来就开始进行计算:首先会把a压入栈中(其实时把局部变量表里的值压入栈中);然后把b压入栈中;接着将栈顶的两个元素先出栈,相加之后再入栈,此时栈中只有一个计算结果300;接下来再把c压入栈中;然后,把栈顶的两个元素出栈,执行完乘法之后再入栈,所以最终栈中只有一个90000;最后,使用ireturn指令结束方法并将栈顶的元素返回给方法的调用者。 总结以上就是虚拟机执行子系统的一个过程,包含了从编译出的Class文件,到被加载到内存中、验证、初始化等,到最终在虚拟机中被执行等完整的过程。这里只是总结和梳理了相关的基础的知识点,在虚拟机中实际的执行过程肯定远比我们上述的内容更加复杂和精彩。