Java 类加载

类加载全过程:

  1. 加载:类加载过程的一个阶段:通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个 Class 对象
  2. 验证:目的在于确保 Class 文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证,文件格式验证,元数据验证,字节码验证,符号引用验证。
  3. 准备:为类变量 (即 static 修饰的字段变量) 分配内存并且设置该类变量的初始值即 0(如 static int i=5; 这里只将 i 初始化为 0,至于 5 的值将在初始化时赋值),这里不包含用 final 修饰的 static,因为 final 在编译的时候就会分配了,对于一般的成员变量是在类实例化时候,随对象一起分配在堆内存中。
  4. 解析:主要将常量池中的符号引用替换为直接引用的过程。符号引用就是一组符号来描述目标,可以是任何字面量,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。有类或接口的解析,字段解析,类方法解析,接口方法解析 (这里涉及到字节码变量的引用,如需更详细了解,可参考《深入 Java 虚拟机》)。
  5. 初始化:类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量 (如前面只初始化了默认值的 static 变量将会在这个阶段赋值,成员变量也将被初始化)。

Java 系统自带三个类加载器:

  • Bootstrap ClassLoader 最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib 下的 rt.jar、resources.jar、charsets.jar 和 class 等。另外需要注意的是可以通过启动 jvm 时指定 - Xbootclasspath 和路径来改变 Bootstrap ClassLoader 的加载目录。比如java -Xbootclasspath/a:path被指定的文件追加到默认的 bootstrap 路径中。
  • Extension ClassLoader 扩展的类加载器,加载目录 %JRE_HOME%\lib\ext 目录下的 jar 包和 class 文件。还可以加载-D java.ext.dirs选项指定的目录。
  • Appclass Loader 也称为 SystemAppClass 加载当前应用的 classpath 的所有类。

父加载器不是父类:

  • 继承关系如图:
  • AppClassLoader 的 parent 是 ExtClassLoader
  • ExtClassLoader 的 parent 是 Bootstrap ClassLoader(ExtClassLoader 直接 getParent() 返回为 null 是因为 Bootstrap ClassLoader 是由 C/C++ 编写的,是虚拟机的一部分,无法在 Java 代码中直接获取它的引用)
  • Bootstrap 没有父加载器

双亲委派:

首先判断这个 class 是不是已经加载成功,如果没有的话,先通过父加载器向上递归,如果 Bootstrap ClassLoader 也没有加载过此 class 实例,那么它就会从它指定的路径中去查找,如果查找成功则返回,如果没有查找成功则交给子类加载器

  1. 一个 AppClassLoader 查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。
  2. 递归,重复第 1 部的操作
  3. 如果 ExtClassLoader 也没有加载过,则由 Bootstrap ClassLoader 出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是sun.mic.boot.class下面的路径。找到就返回,没有找到,让子加载器自己去找
  4. Bootstrap ClassLoader 如果没有查找成功,则 ExtClassLoader 自己在java.ext.dirs路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找
  1. ExtClassLoader 查找不成功,AppClassLoader 就自己查找,在java.class.path路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常

双亲委派优势:

  1. Java 类随着它的类加载器一起具有了层级关系,可避免重复加载
  2. 确保 java 核心 api 中定义类型不会被随意替换(父加载器已加载)

核心方法:

  • loadClass(String, boolen)  //双亲委派实现方法(回溯方式向上向下递归)

执行 findLoadedClass(String) 去检测这个 class 是不是已经加载过了执行父加载器的 loadClass() 方法。如果父加载器为 null,则 jvm 内置的加载器去替代,也就是 Bootstrap ClassLoader如果向上委托父加载器没有加载成功,则通过 findClass(String) 查找如果 class 在上面的步骤中找到了,参数 resolve 又是 true 的话,那么 loadClass() 又会调用 resolveClass(Class) 这个方法来生成最终的 Class 对象

  • findClass(String)  //查找目标类
  • 由各级类加载器重写抛出 ClassNotFoundException 异常通常会调用 defineClass() 方法
  • defineClass(byte[] b, int off, int len)  //将 byte 字节流解析成 JVM 能够识别的 Class 对象
  • 不仅能够通过 class 文件实例化 class 对象,也可通过网络接收一个类的字节码,转换为 byte 字节流

自定义 ClassLoader (需要动态加载一些东西时):

  1. 编写一个类继承自 ClassLoader 抽象类
  2. 重写它的 findClass() 方法
  3. 在 findClass() 方法中调用 defineClass()

应用:

  1. Class 解密类加载器
  2. 热修复

注意:

  1. 自定义类加载器无法加载 Java API 包中的类,需要访问权限,强制加载会抛异常:java.lang.SecurityException: Prohibited package name: java.lang
  2. 并不是所有类加载器都遵循双亲委托,比如线程上下文类加载器(contextClassLoader),Java 核心代码内部去调用外部实现类,比如 jdbc