我们知道Java代码编译后生成的是字节码,那虚拟机是如何加载这些class字节码文件的呢?加载之后又是如何进行方法调用的呢?
Java有一个口号叫做一次编写,到处运行。实现这个口号的就是可以运行在不同平台上的虚拟机和与平台无关的字节码。这里要注意的是,虚拟机也是中立的,只要是符合规范的字节码,都可以被虚拟机接受,例如Groovy,JRuby等语言,都会生成符合规范的字节码,然后被虚拟机所运行,虚拟机不关心字节码由哪种语言生成。
class类文件是一组以8位字节为基础的二进制流,它包含以下几个部分:
类从被加载到虚拟机内存到卸载出内存的生命周期包括:加载->连接(验证->准备->解析)->初始化->使用->卸载。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在java虚拟机中的唯一性。举一个例子:
结果为:

可以看到,由自定义的加载类只能获取同包下的class,而系统的class不能被加载,而且由Class.forName()获取的类与自定义加载类得到的类不是同一个类。
根据五种初始化的条件,父类也会被初始化,但是,上边的代码运行结果显示,父类和接口都没有被初始化,这又是怎么回事呢?
系统提供了三种类加载器,分别是:
我们自定义的ClassLoader继承自应用程序类加载器,当自定义类加载器找不到所加在的类时,会使用启动类加载器进行加载,当启动类加载器加载不到时,由扩展类加载,扩展类加载不到时有应用程序类加载。这也是为什么上边的代码能够成功运行的原因。

栈帧是虚拟机栈的栈元素,栈帧存储了局部变量表,操作数栈,动态连接,返回地址等信息。每一个方法的调用都对应着一个栈帧在虚拟机栈中的入栈和出栈。
Java是一门面向对象的语言,它具有多态性。那么虚拟机又是如何知道运行时该调用哪一个方法?
动态分派的实现:当调用一个对象的方法时,会将该对象的引用压栈到???作数栈,然后字节码指令invokevirtual会去寻找该引用实际类型。如果在实际类型中找对应的方法,且访问权限足够,则直接返回该方法引用,否则会依照继承关系对父类进行查找。实际上,如果子类没有重写父类方法,则子类方法的引用会直接指向父类方法。
不管是解释型语言还是编译型语言,机器都无法理解非二进制语言。高级语言转化成机器语言都遵循现代经典编译原理。即执行前对程序源码进行词法和语法分析,构建抽象语法树。C语言等编译型语言会由单独的执行引擎做这些工作,而Java语言等解释型语言语法抽象树由jvm完成。jvm可以选择通过解释器来解释字节码执行还是通过优化器生成机器代码来执行。
常用的两套指令集架构分别是基于栈的指令集和基于寄存器的指令集。
基于栈的指令集更多的通过入栈出栈来实现计算功能,例如1+1
基于寄存器的指令集更多的是使用寄存器来进行操作,例如1+1
总体来说,基于栈的指令集会慢一些,但是它与寄存器无关,更容易实现到处运行的目标。
又到了该总结的时候了,类加载机制面试中很容易被问到,不幸的是,当时我并没有看这方面的知识。
class类文件结构的每一个部分都可以再深入下去,类文件结构是采用结构体的方式存储的,那么怎么知道集合的长度,各个属性又是怎么被标记的。
类加载机制中有且仅有的五种触发初始化的情况。类加载器的分类。
栈帧的结构,以及方法调用。
Java语言的方法调用分为静态多分派,动态单分派。