首先要厘清一个问题,为什么JAVA需要类加载。不明白这个问题,直接说加载机制就是空中楼阁。
JAVA程序员用编程工具编写的代码生成的都是拓展名为.java的文件,显然这个文件是不能直接被计算机识别并运行里面程序的,需要经过Java编译器编译成拓展名为.class的文件,.class文件中保存着Java代码经转换后的虚拟机指令。
当需要使用某个类时,JAVA虚拟机将会加载它的.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程称为类加载。
加载需要加载器,JAVA虚拟机提供三种预定义类型的类加载器。
启动类加载器
Bootstrap ClassLoader:启动类加载器是用本地代码实现的类加载器,它负责将JAVA_HOME/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。由于启动类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用。具体可由启动类加载器加载到的路径可通过System.getProperty(“sun.boot.class.path”)
查看。
扩展类加载器
Extension ClassLoader:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将JAVA_HOME /lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器,具体可由扩展类加载器加载到的路径可通过System.getProperty("java.ext.dirs")
查看。
系统类加载器
App ClassLoader:系统类加载器是由 Sun 的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径,如第四节中的问题6所述)下的类库加载到内存中。开发者可以直接使用系统类加载器,具体可由系统类加载器加载到的路径可通过System.getProperty("java.class.path")
查看。
当然还可以自定义自己的类加载器,系统的ClassLoader只会加载指定目录下的".class"文件,如果你想加载自己的".class"文件,那么就可以自定义一个ClassLoader。具体方式留待下文再讲
类加载器虽然只用于实现类的加载动作,但是对于任意一个类,都需要由加载它的类加载器和这个类本身共同确立其在Java虚拟机中的唯一性。通俗的说,JVM中两个类是否“相等”,首先就必须是同一个类加载器加载的,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要类加载器不同,那么这两个类必定是不相等的。
好了有了类加载器,是不是就可以加载了呢,且慢着急,先了解一下类加载器的加载模型,也就是双亲委派模型:
当一个类加载器收到类加载任务时,立即将任务委派给它的父类加载器去执行,直至委派给最顶层的启动类加载器为止。如果父类加载器无法加载委派给它的类时,将类加载任务退回给它的下一级加载器去执行;
除了启动类加载器以外,每个类加载器拥有一个父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码。用户的自定义类加载器的父类加载器是AppClassLoader;
器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。
好了 ,说完了类加载器和它们的双亲委派模型,下面该说下类加载过程了。
类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。
所谓加载,简而言之就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象。所谓类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用
反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来,则 JVM 在运行期也无法反射
加载阶段,简言之,查找并加载类的二进制数据,生成 Class 的实例
在加载类时,Java 虚拟机必须完成以下3件事情:
当类加载到系统后,就开始链接操作链接操作的第一步是验证
它的目的是保证加载的字节码是合法、合理并符合规范的
验证的步骤比较复杂,实际要验证的项目也很繁多,大体上 Java 虚拟机需要做以下检查,如图所示
整体说明:
验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证,以及符号引用验证等
链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查
具体说明:
准备阶段(Preparation),简言之,为类的静态变量分配内存,并将其初始化为默认值
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。
Java 虚拟机为各类型变量默认的初始值:
注意:Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故对应的,boolean 的默认值就是 false
注意:
/**
*
* 基本数据类型:非 final 修饰的变量,在准备环节进行默认初始化赋值
* final 修饰以后,在准备环节直接进行显式赋值
*
* 拓展:如果使用字面量的方式定义一个字符串的常量的话,也是在准备环节直接进行显式赋值
*/
public class LinkingTest {
private static long id;
private static final int num = 1;
public static final String constStr = "CONST";
public static final String constStr1 = new String("CONST");
}
在准备阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用
符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存分布无关。比较容理解的就是在 Class 类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下 println() 方法被调用时,系统需要明确知道该方法的位置
举例:输出操作 System.out.println() 对应的字节码:
invokevirtual #24
任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便“万事俱备,只欠东风”,就等着开发者使用了
开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用 new 关键字为其创建对象实例
类被加载、链接和初始化后,它的生命周期就开始了。当类的 Class 对象不再被引用,即不可触及时,Class 对象就会结束生命周期, 类在方法区内的数据也会被卸载,从而结束类的生命周期
一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期
注意:
综合以上三点,一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下,来实现系统中的特定功能
类的加载器和加载机制就说到这里,看官如果觉得有收获,就请点赞收藏鼓励吧