JAVA面试必看,基础知识之类加载器和加载机制

首先要厘清一个问题,为什么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;

JAVA面试必看,基础知识之类加载器和加载机制_第1张图片

器要加载这个类,最终都是委派给处于模型最顶端的Bootstrap ClassLoader进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型而是由各个类加载器自行加载的话,如果用户编写了一个java.lang.Object的同名类并放在ClassPath中,那系统中将会出现多个不同的Object类,程序将混乱。因此,如果开发者尝试编写一个与rt.jar类库中重名的Java类,可以正常编译,但是永远无法被加载运行。

好了 ,说完了类加载器和它们的双亲委派模型,下面该说下类加载过程了。

类从被加载到JVM中开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。

其中类加载过程包括加载、验证、准备、解析和初始化五个阶段。

JAVA面试必看,基础知识之类加载器和加载机制_第2张图片

一、加载阶段

所谓加载,简而言之就是将 Java 类的字节码文件加载到机器内存中,并在内存中构建出 Java 类的原型——类模板对象。所谓类模板对象,其实就是 Java 类在 JVM 内存中的一个快照,JVM 将从字节码文件中解析出的常量池、类字段、类方法等信息存储到模板中,这样 JVM 在运行期便能通过类模板而获取 Java 类中的任意信息,能够对 Java 类的成员变量进行遍历,也能进行 Java 方法的调用

反射的机制即基于这一基础。如果 JVM 没有将 Java 类的声明信息存储起来,则 JVM 在运行期也无法反射

加载阶段,简言之,查找并加载类的二进制数据,生成 Class 的实例

在加载类时,Java 虚拟机必须完成以下3件事情:

  • 通过类的全名,获取类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构(Java 类模型)
  • 创建 java.lang.Class 类的实例,表示该类型。作为方法区这个类的各种数据的访问入口 

二、链接阶段

1:验证

当类加载到系统后,就开始链接操作链接操作的第一步是验证

它的目的是保证加载的字节码是合法、合理并符合规范的

验证的步骤比较复杂,实际要验证的项目也很繁多,大体上 Java 虚拟机需要做以下检查,如图所示

JAVA面试必看,基础知识之类加载器和加载机制_第3张图片

整体说明:

验证的内容则涵盖了类数据信息的格式验证、语义检查、字节码验证,以及符号引用验证等

  • 其中格式验证会和加载阶段一起执行。验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中
  • 格式验证之外的验证操作将会在方法区中进行

链接阶段的验证虽然拖慢了加载速度,但是它避免了在字节码运行时还需要进行各种检查

具体说明:

  1. 格式验证:是否以魔数 0xCAFEBABE 开头,主版本和副版本号是否在当前 Java 虚拟机的支持范围内,数据中每一个项是否都拥有正确的长度等
  2. Java 虚拟机会进行字节码的语义检查,但凡在语义上不符合规范的,虚拟机也不会给予验证通过。比如:
  3. 是否所有的类都有父类的存在(在 Java 里,除了 Object 外,其他类都应该有父类)
  4. 是否一些被定义为 final 的方法或者类被重写或继承了
  5. 非抽象类是否实现了所有抽象方法或者接口方法
  6. 是否存在不兼容的方法(比如方法的签名除了返回值不同,其他都一样,这种方法会让虚拟机无从下手调度;absract 情况下的方法,就不能是final 的了)
  7. Java 虚拟机还会进行字节码验证,字节码验证也是验证过程中最为复杂的一个过程。它试图通过对字节码流的分析,判断字节码是否可以被正确地执行。比如:
  8. 在字节码的执行过程中,是否会跳转到一条不存在的指令
  9. 函数的调用是否传递了正确类型的参数
  10. 变量的赋值是不是给了正确的数据类型等

2:准备

准备阶段(Preparation),简言之,为类的静态变量分配内存,并将其初始化为默认值

当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。

Java 虚拟机为各类型变量默认的初始值:

JAVA面试必看,基础知识之类加载器和加载机制_第4张图片

注意:Java 并不支持 boolean 类型,对于 boolean 类型,内部实现是 int,由于 int 的默认值是0,故对应的,boolean 的默认值就是 false

注意:

  1. 这里不包含基本数据类型的字段用 static final 修饰的情况,因为 final 在编译的时候就会分配了,准备阶段会显式赋值
  2. 注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到 Java 堆中
  3. 在这个阶段不会像初始化阶段中那样会有初始化或者代码被执行
/**
 * 

* 基本数据类型:非 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"); }

3:解析

在准备阶段(Resolution),简言之,将类、接口、字段和方法的符号引用转为直接引用

符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存分布无关。比较容理解的就是在 Class 类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行时,只有符号引用是不够的,比如当如下 println() 方法被调用时,系统需要明确知道该方法的位置

举例:输出操作 System.out.println() 对应的字节码:

invokevirtual #24

三:使用阶段

任何一个类型在使用之前都必须经历过完整的加载、链接和初始化3个类加载步骤。一旦一个类型成功经历过这3个步骤之后,便“万事俱备,只欠东风”,就等着开发者使用了

开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用 new 关键字为其创建对象实例

四:卸载阶段

类被加载、链接和初始化后,它的生命周期就开始了。当类的 Class 对象不再被引用,即不可触及时,Class 对象就会结束生命周期, 类在方法区内的数据也会被卸载,从而结束类的生命周期

一个类何时结束生命周期,取决于代表它的 Class 对象何时结束生命周期

注意:

  1. 启动类加载器加载的类型在整个运行期间是不可能被卸载的(JVM 和 JSL 规范)
  2. 被系统类加载器和扩展类加载器加载的类型在运行期间不太可能被卸载,因为系统类加载器实例或者扩展类的实例基本上在整个运行期间总能直接或者间接的访问的到,其达到 unreachable 的可能性极小
  3. 被开发者自定义的类加载器实例加载的类型只有在很简单的上下文环境中才能被卸载,而且一般还要借助于强制调用虚拟机的垃圾收集功能才可以做到。可以预想,稍微复杂点的应用场景(比如:很多时候用户在开发自定义类的加载器实例的时候采用缓存的策略以提高系统性能),被加载的类型在运行期间也是几乎不太可能被卸载的(至少卸载的时间是不确定的)

综合以上三点,一个已经加载的类型被卸载的几率很小至少被卸载的时间是不确定的。同时我们可以看的出来,开发者在开发代码时候,不应该对虚拟机的类型卸载做任何假设的前提下,来实现系统中的特定功能

类的加载器和加载机制就说到这里,看官如果觉得有收获,就请点赞收藏鼓励吧

你可能感兴趣的:(java,jar,开发语言)