JVM Basic - ClassLoader

2022-03-23
2 min read
java , jvm

类加载顾名思义就是把类加载到 JVM 中,而输入一段二进制流到内存,之后经过一番解析、处理转化成可用的 class 类,这就是类加载要做的事情。

二进制流可以来源于 class 文件,或者通过字节码工具生成的字节码或者来自于网络都行,只要符合格式的二进制流,JVM 来者不拒。

类加载流程分为加载、连接、初始化三个阶段,连接还能拆分为:验证、准备、解析三个阶段。

类加载器(ClassLoader)

什么是类加载器

类加载器是 JRE 的一部分,负责在运行时将 Java 类动态加载到 JVM,有了类加载器,JVM 无需了解底层文件或文件系统即可运行Java程序。

类加载器不会一次将全部类加载到内存里,而是在程序需要时加载。

JVM 内置的类加载器类型

  • Bootstrap class loader

    每个 JVM 实现必须有一个 bootstrap 类加载器。它负责 JDK 内部类,通常是 rt.jar 和位于 JAVA_HOME/jre/lib 目录下的其他核心类,该路径通常被称为引导路径(bootstrap path)。该类加载器由 C/C++ 语言实现。

  • Extension class loader

    Bootstrap 的子类加载器,它加载扩展目录 JAVA_HOME/jre/lib/extjava.ext.dirs 指定路径下的类。它在 sun.misc.Launcher$ExtClassLoader 类中由 Java 语言实现。

  • System/Application class loader

    Extension 的子类加载器。它负责加载 App 层级的类,它加载在 classpath 环境变量、-classpath、-cp 命令行选项中找到的文件。它在 sun.misc.Launcher$AppClassLoader 类中由 Java 语言实现。

自定义的类加载器

在需要从本地或网络加载类的场景,我们可能需要使用自定义类加载器。

使用场景

自定义类加载器不仅对在运行时加载类有帮助,还包括:

  • 修改现有字节码,如织入代理
  • 动态创建适合用户需求的类。如 JDBC 中,通过动态类加载完成不同驱动实现(driver implementations)之间的切换。
  • 实现类版本控制机制,如为具有相同名称和包名的类加载不同的字节码(热修复)。这可以通过 URL 类加载器或自定义加载器完成。

我们可以自定义类加载器实现指定类的加载:

package com.zac4j.system.util

import java.io.ByteArrayOutputStream
import java.io.File
import java.io.IOException

/**
 * Custom ClassLoader sample
 *
 * @author: zac
 * @date: 2020/7/19
 */
internal object Test {
  @JvmStatic fun main(args: Array<String>) {
    val classLoader = CustomClassLoader()
    val clazz = classLoader.loadClass("com.zac4j.system.util.Utils")
    println("custom classloader:${clazz.canonicalName}")
  }
}

/**
 * 自定义 ClassLoader,加载指定类文件。
 */
class CustomClassLoader : ClassLoader() {
  override fun findClass(name: String): Class<*> {
    val b = loadClassFromFile(name)
    return defineClass(name, b, 0, b.size)
  }

  /**
   * 根据类名加载类文件
   */
  private fun loadClassFromFile(filename: String): ByteArray {
    val inStream = javaClass.classLoader?.getResourceAsStream(
        filename.replace('.', File.separatorChar) + ".class"
    ) ?: return byteArrayOf()
    val outStream = ByteArrayOutputStream()
    try {
      while (inStream.read() != -1) {
        val nextVal = inStream.read()
        outStream.write(nextVal)
      }
    } catch (e: IOException) {

    }
    return outStream.toByteArray()
  }
}

类加载器是如何工作的

类加载器是 JRE 的一部分。当 JVM 需要一个类时,类加载器尝试定位该类,并使用 完全限定的类名 (fully qualified class name)类定义(class definition) 加载到 Runtime 中。

java.lang.ClassLoader.loadClass() 方法负责将 class definition 加载到 Runtime 中。如果请求的类尚未加载,则它将请求委派给父类加载器,此过程递归进行。如果父类加载器找不到该类,则子类加载器调用 java.net.URLClassLoader.findClass() 方法在文件系统中查找类。最终,如果子类加载器未找到该类,则它将抛出 java.lang.NoClassDefFoundErrorjava.lang.ClassNotFoundException

使用代理模型

类加载器遵循代理模型,在该模型中,类加载器会根据请求查找的类,将搜索类的工作委托给父加载器处理。仅当 bootstrap class loader 和 extension class loader 都未能加载该类时,system class loader 才会尝试自己加载该类。

类加载器的功能

类加载器系统主要有3种功能

  • 加载(Loading)

    类加载器读取 .class 文件,生成对应的二进制数据并保存到 JVM 方法区。对每个 .class 文件,JVM 在方法区保存这些信息:

    • 已加载类和其父类的完全限定名
    • .class 文件是否与 Class 或 Interface 或 Enum 相关
    • 修饰符(Modifier)、变量和方法信息等。

    加载 .class 文件后,JVM 在堆中创建该文件对应的 Class 类型对象。可以使用此 Class 对象来获取类级别(class level)的信息,如类名、父类名、方法(Class.getdeclaredMethods())和变量信息(Class.getDeclaredFields())等。

  • 连接(Linking)

    连接可分为:验证(Verification)、准备(Preparation)、解析(Resolution)。

    • 验证:通过检查 .class 文件是否正确的格式化和是否由有效的编译器生成,确保该文件的正确性。如果验证失败,将抛出运行时异常:java.lang.VerifyError
    • 准备:JVM 为类变量分配内存,并将内存初始化为默认值。
    • 解析:这是用直接引用(direct references)替换类型中的符号引用(symbolic references)的过程。通过搜索方法区域以找到引用的实体来完成此操作。
  • 初始化(Initialization)

    这个阶段将为所有静态变量分配在代码或静态代码块中定义的值。在类中从顶至底(from top to bottom)执行,在类层级中从父类到子类(from parent to child)执行。类加载器的初始化:Bootstrap class loader -> Extension class loader -> App class loader.