话不多说,干就完了。
先上图,由图中可以看到,整个虚拟机大致由三个子系统组成 类加载子系统、运行时数据区、执行引擎 。
首先要讲的就是类加载子系统。
要说类加载子系统,那就离不开虚拟机的类加载机制。可以这么认为,一切Java程序的运行,首先从类加载开始。Java文件会被编译成class字节码文件,再由Java虚拟机将class文件加载到内存中经过一系列的流程,最终变成可以被虚拟机直接使用的Java对象类型。
所谓类加载机制就是Java虚拟机由Java文件编译之后用来描述类信息的class字节码文件,通过类加载子系统对类进行加载、验证、准备、解析等过程,最终转换成虚拟机可以使用的Java对象类型,这就是所谓的类加载机制。而在类加载机制中,最为重要的就是 类加载子系统 。
类加载子系统负责动态的加载类,在程序运行时,当一个类被初次使用时,它将被类加载子系统加载、连接、初始化。最终加载到内存中被使用。它只 负责类的加载 ,class文件能否执行取决于执行引擎。
被加载的类信息将会存放在方法区的运行时常量池中。
类的加载过程,也就是类的生命周期,分为七个过程, 加载、验证、准备、解析、初始化、使用、卸载 这么七个过程,其中 验证、准备、解析 通常又被合在一起称为 连接 过程。
针对上面每个过程,下面来进行具体的分析,每个过程做了什么。
在说加载之前,首先区分一个概念,“加载”和“类加载”, “加载”只是“类加载”流程中的一个阶段 。在加载阶段,虚拟机会完成以下三件事:
验证是连接阶段的第一步,主要目的就是为了 确保class文件的正确性 ,保证class文件中包含的信息符合虚拟机的要求并且不会危害到虚拟机自身安全。主要包含以下四种类型的验证:
在说到加载阶段的时,会将类的静态存储结构存放到方法区,而准备阶段实际上就是为 类的变量分配内存并设置类变量的初始默认值 。
在此阶段,需要注意两个概念:
分配内存:分配内存仅分配类变量(即被static修饰的变量,但不包含final关键字修饰的static变量,因为final修饰的在编译的时候就已经分配了内存),并不包含类实例变量(实例变量通常只会在对象实例化的时候分配在堆中)
设置初始默认值:通常情况下就是设置数据类型的零值。
private static int a = 1;//类变量 在准备阶段只会被赋值为0,在后面会说到初始化阶段时才会被赋值为1 private static final int i = 1;// 被final修饰的static变量在编译时就被分配为2 private int b; //实例变量 复制代码
数据类型 | 零值 |
---|---|
int | 0 |
double | 0.0d |
float | 0.0f |
long | 0l |
short | (short)0 |
char | '/u0000' |
boolean | False |
byte | (byte)0 |
refrence | null |
解析阶段就是常量池中的符号引用转换成直接引用的过程。解析动作主要正对类或接口、字段、类方法、接口方法、方法类型等。事实上,解析动作往往伴随着虚拟机执行初始化之后才执行。
初始化阶段可以看作是类加载过程中的最后一步。初始化阶段主要执行类构造器方法<clint>方法的过程。
它是由编译器自动收集类中的所有 类变量的赋值动作(显示初始化) 和 静态代码块中的语句 合并而来的,也就是说如果类中没有没static修饰的变量赋值动作或者没有静态代码块,那么在编译的时候也就不会生成<clinit>方法。在类构造器方法中,它的 执行顺序按照语句在源代码中的顺序来执行的 。
通过上图的类测试结果,也验证了<clinit>方法是由编译器自动收集类中的所有 类变量的赋值动作(显示初始化) 和 静态代码块中的语句 合并而来的。
<clinit>不同于类的构造函数<init>(或者说实例构造器), 虚拟机会保证在子类的<clinit>执行之前,父类的<clinit>方法就执行完成了 。通俗的说,若当前类继承了父类,那么虚拟机会保证在执行该类的<clinit>方法之前,它所继承的父类的类构造器方法已经执行完毕了。
在对类完成一系列的初始化操作后,程序就能够使用对应的类。类的使用分为 主动使用 和 被动使用 。除了主动使用以外的方式都被称为被动使用。主动住使用主要有以下其中方式:
java.lang.invoke.MethodHandle
实例的解析结果, REF_getStatic
、 REF_putStatic
、 REF_invokeStatic
句柄对应的类没有初始化,则初始化) 回到开头的类加载过程图中,对其进行总结补充如下图:
不怕路歹行不怕大雨淋,心上一字敢 面对我的梦,甘愿来作憨人。 --<憨人>