JVM类加载机制
类文件结构
虚拟机和字节码存储格式是实现语言无关性的基础。
class文件是一组以8个字节为基础单位的二进制流,大端形式。
class文件其实也可以在内存、网络等媒介中存在。

魔数:CA FE BA BE
常量池:表类型,占用空间大。存放字面量和符号引用。
字面量:类似于常量,如文本字符串。
符号引用:含各种描述符、句柄等。
表结构以常量类型标志位
u1开始。对应的17种类型各自有独立的数据结构。
访问标志:识别类/接口层次的访问信息(public/abstract/final)
类索引、父类索引、接口索引集合。用于确定继承关系。
字段表集合。描述接口或类中声明的变量。包括:作用域、常量池引用、属性表。
字段:类级变量、实例级变量,不包括方法内部声明的局部变量。
方法表集合。结构大致同字段表。
方法的特征签名:不包括返回值。
属性表集合。用于描述某些场景专有的信息。
类加载机制

加载在什么时机发生呢?逼不得已的时候(主动引用)
主动引用:
new、设置static字段、调用类的静态方法、初始化一个父类还没初始化的类(初始化父类)、反射调用。被动引用:子类引用父类静态字段不会导致子类初始化。
接口与类不同的点在于,只有真正使用到父接口时,才初始化
类加载过程
加载
完成三件事情:
通过类的全限定名获取定义此类的二进制字节流
将字节流所代表的静态存储结构转换为方法区的运行时数据结构
在内存中生成一个代表该类的
Class对象,作为方法区的数据访问入口
对于非数组类型的加载,开发人员可以定义自己的类加载器。对于数组类,由Java虚拟机直接在内存中动态构造,是引用类则递归加载组件,否则再经过引导类加载器关联(确定类型)。
验证
这是连接阶段的第一步,包括:文件格式验证、元数据验证、字节码验证、符号引用验证。
准备
正式为类中定义的静态(不包括实例变量)变量分配方法区内存和置初始值。
定义时的赋值指令会被放在 <clinit>() 方法中,在初始化时执行,即只有在初始化后才等于非初始值。
解析
将常量池中的符号引用替换为直接引用。对于
类/接口:非数组 - 加载器;数组 - 虚拟机生成
字段:解析索引符号,否则搜索父类引用。
方法:类型匹配则查找父类引用、匹配方法,否则失败
初始化
是执行初始化方法 <clinit> ()方法的过程。这个方法是编译后自动生成的:
包括变量赋值动作、静态语句块(不可访问)。顺序同代码顺序
父类初始化先行,初始化阶段不能进行元素访问
类加载器
类加载器,即获取所需的类的动作的代码。
类与类加载器
只有在同一个加载器加载下,比较两个类是否相等才有意义。
这里的“相等”,包括equals isAssignableFrom isInstance方法的返回结果。
双亲委派模型
Java虚拟机存在两种不同的类加载器。一种是启动类加载器,来自机器自身;另一种是其他所有的类加载器,独立存在于机器外部并继承于ClassLoader抽象类。
其工作过程是:一个类加载器收到类加载的请求,先不自己尝试加载类,而是先将请求委派给父类加载器(loadClass)完成。(所有的类加载请求最终都应该传送到启动类加载器)只有当父类加载器反馈无法完成加载时,子加载器才会尝试自己去自己负责的区域完成加载(findClass)。
会先自底向上检查类是否被加载,再自上而下地尝试加载类。
优点:使得Object类在任何类加载器环境中均为同一个类。
破坏双亲委派模型
自定义加载器的话,需要继承 ClassLoader 。
如果不想打破双亲委派模型,重写
ClassLoader类中的findClass()方法即可。
模块化系统下的类加载器
Java 9 引入了模块化特性。模块module就是代码和数据的封装体,代码是指一些包括类型的Packages。Package是一些类路径名字的约定,而模块是一个或多个Packages组成的一个封装体。module可以实现有限范围内的代码public访问权限,将代码公开区分为:模块外部有限范围的公开访问和模块内部的公开访问。
模块化与之对应地进行了加载器的更新处理。

字节码执行引擎
运行时栈帧结构

局部变量表
存在方法参数和方法内部定义的局部变量,其容量以变量槽为最小单位。

为节省空间,变量槽可重用。如果当前字节码PC超出变量的作用域,这个变量的槽可交给别的变量来重用。
只要其他变量没有使用这部分 Slot 区域,这个变量就还保存在那里,而对于垃圾回收,需要局部变量表中的 Slot 不再存在关于 placeholder 的引用。
局部变量不像类变量存在准备阶段,无默认值,定义时建议赋予初始值
操作数栈
与字节码指令序列严格匹配,每个栈容量为32位(double类型数据栈容量为2)
两个栈帧之间,各自对应的栈顶(操作栈共享区域)和栈底(局部变量表共享区域)重叠,可实现数据共享。
动态连接
常量池中大量的符号引用,一部分在类加载阶段或首次使用时即转换成直接引用,另外一部分将在运行期间转换为直接引用。后者即称为动态连接。
方法返回地址
方法被退出,只有两种方式:字节码返回指令、异常。
方法调用
方法调用不是说方法被执行,其任务是确定调用哪一个方法(版本)。与之对应地,涉及了多态的机制。
解析
类加载的解析阶段就会将一部分符号引用转化为直接引用,即确定了方法的调用版本。对于这些调用目标在代码中写好、编译时即确定的方法调用过程就叫做解析调用。
解析调用一定是个静态过程。
分派
相对地,分派是一个动态过程。
静态分派(重载)
静态类型:其变化仅仅在使用时发生,静态类型本身不会被改变。
实际类型:编译阶段是不定量,直到运行时才能确定。
重载时,是以静态类型作为判定依据的。重载就是编译时多态的一个例子。我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,运行时才能确定。
依赖静态类型决定方法运行版本的分派,为静态分派。
动态分派(重写)
从所有者逐级向上寻找对应的方法。在运行期确定方法执行版本的分派称为动态分派。这种多态性的根源在于虚方法调用指令invokevirtual执行逻辑。
虚方法:使用了 virtual 修饰符,这种函数或方法可以被子类继承和覆盖,通常使用动态分派实现。函数可以给出目标函数的定义,但该目标的具体指向在编译期可能无法确定。不允许再有 static、abstract 或者 override 修饰符。
需要记住,字段永远不参加多态。Java中有虚方法存在,则字段不可能是虚的。当子类声明了与父类同名的字段时,在子类内存中两个字段均会存在,而子类的字段会遮蔽父类的同名字段。
如何实现呢?
动态分派在虚拟机中执行得非常频繁,为减少频繁搜索元数据,一种优化策略是虚方法表,存放各个方法的实际入口地址,再根据是否存在重写进行下一步处理。
单分派与多分派
除了静态分派和动态分派的分类方式,还可以根据宗量将方法分派分为单分派和多分派两类。
宗量:方法的接收者与方法的参数的统称。
静态分派属于多分派,根据方法接收者的静态类型和方法参数类型两个宗量进行选择。
动态分派属于单分派,只根据方法接收者的实际类型一个宗量进行选择。
动态类型语言支持
动态类型语言就是类型检查的主体过程在运行期进行,而非编译期的编程语言。如:JavaScript/PHP/Python。
Java引入指令invokedynamic和invoke包,使得一些方法调用可以“跨域”调用。Lambda 表达式就是通过 invokedynamic 指令实现的。

基于栈的字节码解释执行引擎

基于栈的指令集与基于寄存器的指令集
Javac编译器输出的字节码指令流,基本上是一种基于栈的ISA,大部分依赖操作数栈进行工作。(基于寄存器:二地址,依赖寄存器)
优点:可移植、代码紧凑、编译简单(所需的空间都在栈上操作)
缺点:理论上执行速度稍慢。
解释器执行过程
中间变量都以操作数栈的出入栈作为信息交换途径。
案例:Tomcat
为解决web应用的可见性问题,其重写了类加载器:

参考: Github TangBean的思维导图
《深入理解JAVA虚拟机》
最后更新于
这有帮助吗?