🟧1. 类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:、
加载(Loading)
验证(Verification)
准备(Preparation)
解析(Resolution)
初始化(Initialization)
使用(Using)
卸载(Unloading)
其中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,加载过程必须按这种顺序进行。由于 Java 语言支持运行时绑定(动态绑定),在某些情况下,解析阶段可以在初始化之后再开始,这些阶段通常都是互相交叉地混合式进行,通常会在一个阶段执行的过程中调用、激活另一个阶段。
🟧2. 类加载的过程
🟠2.1 加载
在加载阶段,虚拟机需要完成的 3 件事情:
通过一个类的全限定名来获取定义此类的二进制字节流。二进制字节流获取的来源并没有严格指明,可以是 Class 文件、ZIP 包(JAR、EAR、WAR 格式的基础)、网络中获取、运行时计算生成、其他文件(如 JSP 文件)等等。
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
在内存中生成代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有:
_java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴露给 java 使用
_super 即父类
_fields 即成员变量
_methods 即方法
_constants 即常量池
_class_loader 即类加载器
_vtable 虚方法表
_itable 接口方法表
值得注意的的是,加载阶段和链接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,链接阶段可能已经开始。
注意:
instanceKlass 这样的【元数据】是存储在方法区(1.8 后的元空间内),而元空间又位于本地内存中,但 _java_mirror 是存储在堆中
InstanceKlass 和 *.class (Java 镜像类)互相保存了对方的地址
类的对象在对象头中保存了 *.class 的地址,让对象可以通过其找到方法区中的 instanceKlass,从而获取类的各种信息
🟠2.2 链接
🔶2.2.1 验证
验证阶段是为了确保 Class 文件的字节流包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。大致有 4 个阶段的检验动作:
文件格式验证:保证输入的字节流能正确地解析并存储于方法区之内,格式上符合一个 Java 类型信息的要求。
元数据验证:对类的元数据信息进行语义校验,保证不存在不符合 Java 语言规范的元数据信息。
字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
符号引用验证:可以看作是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验,目的是确保解析动作能够正常执行,如果无法通过符号引用验证,那么将会抛出一个 java.lang.IncompatibleClassChangError 异常的子类。
🔶2.2.2 准备
为 static 变量分配空间,并设置 static 变量初始值,这些变量所使用的内存都将在方法区中进行分配。
static 变量在 JDK 7 之前存储于 instanceKlass 末尾,从 JDK 7 开始,存储于 _java_mirror 末尾
static 变量分配空间和赋值是两个步骤,分配空间在准备阶段完成,赋值在初始化阶段完成
如果 static 变量是 final 的基本类型,以及字符串常量,那么编译阶段值就确定了,赋值在准备阶段完成
如果 static 变量是 final 的,但属于引用类型,那么赋值也会在初始化阶段完成
🔶2.2.3 解析
解析是 JVM 将常量池中的符号引用解析为直接引用的过程。
符号引用(Symbolic Reference):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标内存即可。
直接引用(Direct Reference):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。
🟠2.3 初始化
到了初始化阶段,才真正开始执行类中定义的 Java 程序代码,初始化阶段是执行类构造器 clinit() V 方法的过程,虚拟机会保证这个类的『构造方法』的线程安全。
clinit() V 方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
注意:编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问,如:
public class Test { static { i = 0; // 给变量赋值可以正常编译通过 System.out.println(i); // 编译器报错,非法向前引用 } static int i = 1; }
类的初始化的懒惰的,以下情况会初始化:
main 方法所在的类,总会被首先初始化
首次访问这个类的静态变量或静态方法时
子类初始化时,如果父类还没初始化,会先触发父类的初始化
使用 java.lang.reflect 包的方法对类进行反射调用时
使用 new 关键字实例化对象时
不会导致类初始化的情况
访问类的 static final 静态常量(基本类型和字符串)不会触发初始化,因为在编译器已经放入常量池中
子类访问父类的静态字段,只会触发父类的初始化,子类不会初始化
类对象.class 不会触发初始化
创建该类的数组不会触发初始化,创建动作由字节码指令 newarray 触发
类加载器的 loadClass 方法
Class.forName 的参数 2 为 false 时
验证类是否被初始化,可以看改类的静态代码块是否被执行。
🟧3. 类加载器
类加载阶段中的 ”通过一个类的全限定名来获取描述此类的二进制字节流“ 的动作是放到 JVM 外部实现的,实现这个动作的代码模块称为 ”类加载器“。
以 JDK 8 为例,类加载器分为以下几类:
启动类加载器是由 C++ 语言实现,是虚拟机自身的一部分,可通过在控制台输入指令,使得类被启动类加器加载。
其他的类加载器包括 Extension ClassLoader、Application ClassLoader 和自定义类加载器,这些类加载器都由 Java 语言实现,独立于虚拟机外部,并且继承 java.lang.ClassLoader.
🟠3.1 启动类加载器
C/C++ 语言编写,负责将存放在 < JAVA_HOME>\jr\lib\rt.jar 目录中的并能够被虚拟机识别的类库加载到虚拟机内存中,无法被 Java 程序直接引用,没有父加载器。
🟠3.2 扩展类加载器
Java 语言编写,负责加载 < JAVA_HOME>\lib\ext 目录中,或者被 java.exr.dirs 系统变量锁指定的路径中的所有类库,开发者可以直接使用,派生于 ClassLoader.
🟠3.3 系统类加载器
Java 语言编写,负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器,派生于 ClassLoader.
🟠3.4 自定义类加载器
使用场景
防止源码泄漏
扩展加载源
隔离加载类:这些类都希望予以隔离,不同应用的同类名都可以加载,不冲突,常见于 Tomcat 容器
修改类加载的方式:例如,在需要时,可以动态加载
步骤
继承 ClassLoader 父类
遵从双亲委派机制,重写 findClass 方法,不是重写 loadClass 方法,否则不会走双亲委派机制
读取类文件的字节码
调用父类的 defineClass 方法来加载类
使用者调用该类加载器的 loadClass 方法
🟧4. 双亲委派模型
双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器。值得注意的是,类加载器之间的父子关系一般不会以继承的关系实现,而是使用组合关系复用父加载器的代码。
🟠4.1 双亲委派模型的工作过程
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行
如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归请求最终将到达顶层的启动类加载器
如果父类加载器可以完成类加载任务,就成功返回;如果父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制
优点:
Java 类随着它的加载器一起具备了带有优先级的层次关系。
保证类不会重复加载和 Java 程序稳定运作,保证了 Java 核心 API 不会被随意替换,造成混乱。
实现双亲委派模型的代码集中在 java.lang.ClassLoader 的 loadClass 方法中,以下是 loadClass 方法的源码:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先,检查请求的类是否已经被加载过了 Class<?> c = findLoadedClass(name); //如果没有被加载过 if (c == null) { try { //看是否被它的上级加载器加载过了 Extension的上级是Bootstarp,但它显示为null if (parent != null) { c = parent.loadClass(name, false); } else { //看是否被启动类加载器加载过 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { //如果父类加载器抛出 ClassNotFoundException //说明父类加载器无法完成加载请求 } if (c == null) { // 在父类加载器无法加载的时候 // 再调用本身的 findClass 方法进行类加载 c = findClass(name); } } if (resolve) { resolveClass(c); } return c; } }
🟠4.2 破坏双亲委派模型
双亲委派模型的第一次“被破坏”其实发生在双亲委派模型出现之前——即 JDK1.2 面世以前的“远古”时代,建议用户重写 loadClass() 方法
双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷导致的。如果有基础类型又要调用回用户的代码,此时也会破坏双亲委派模型。
双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求而导致的。这里所说的“动态性”指的是一些非常“热”门的名词:代码热替换(Hot Swap)、模块热部署(Hot Deployment)等,也就是希望应用程序能够像计算机外设一样,外设升级或者替换了,不重启机器就能使用。