• Java 入门指南:JVM(Java虚拟机)—— Java 类加载器详解


    类加载器

    类加载器(Class Loader)是 Java 虚拟机(JVM)的一部分,它的作用是将类的字节码文件(.class 文件)从磁盘或其他来源加载到 JVM 中。类加载器负责查找和加载类的字节码文件,并将其转化为 Class 对象。

    类加载器从 JDK 1.0 就出现了,最初只是为了满足 Java Applet(已经被淘汰) 的需要。后来,慢慢成为 Java 程序中的一个重要组成部分,赋予了 Java 类可以被动态加载到 JVM 中并执行的能力。

    根据官方 API 文档的介绍:

    类加载器是一个负责加载类的对象。ClassLoader 是一个抽象类。给定类的二进制名称,类加载器应尝试定位或生成构成类定义的数据。典型的策略是将名称转换为文件名,然后从文件系统中读取该名称的“类文件”。

    每个 Java 类都有一个引用指向加载它的 ClassLoader。但数组类不是通过 ClassLoader 创建的,而是 JVM 在需要的时候自动创建的,数组类通过getClassLoader()方法获取 ClassLoader 的时候和该数组的元素类型的 ClassLoader 是一致的。

    • 类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。

    • 每个 Java 类都有一个引用指向加载它的 ClassLoader

    • 数组类不是通过 ClassLoader 创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。

    组成部分

    在 Java 中,类加载器主要有三个层次:

    1. 启动类加载器(Bootstrap ClassLoader):这是最基础的类加载器,由 C++ 实现,通常表示为 null,并且没有父级,负责加载扩展目录下的 jar 包和系统类路径下的核心库( %JAVA_HOME%/lib 目录下的 rt.jarresources.jarcharsets.jar 等 jar 包和类)以及被 -Xbootclasspath 参数指定的路径下的所有类。

      rt.jar:rt 代表“RunTime”,rt.jar 是 Java 基础类库,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.* 都在里面,比如 java.util.*java.io.*java.nio.*java.lang.*java.sql.*java.math.*

    2. 扩展类加载器(Extension ClassLoader):由 Java 实现,负责加载 Java 默认扩展目录下的 jar 包(%JRE_HOME%/lib/ext 目录下的 jar 包和类以及被 java.ext.dirs 系统变量所指定的路径下的所有类)。

    3. 系统类加载器(System/App ClassLoader):也称为应用程序类加载器,由 Java 实现,负责加载用户类路径(classpath)下的所有 jar 包和类。

    ![[Pasted image 20240915225845.png]]

    除了这三个内置的类加载器外,还可以自定义类加载器,通过继承 java.lang.ClassLoader 类的方式实现,以满足特殊的需求。例如,可以通过自定义类加载器来加载网络上的类,或者从数据库中加载类。

    对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals)。

    ClassLoader

    除了 BootstrapClassLoader 是 JVM 自身的一部分之外,其他所有的类加载器都是在 JVM 外部实现的,并且全都继承自 ClassLoader 抽象类。这样做的好处是用户可以自定义类加载器,以便让应用程序自己决定如何去获取所需的类。

    每个 ClassLoader 可以通过 getParent() 获取其父 ClassLoader,如果获取到 ClassLoadernull 的话,那么该类是通过 BootstrapClassLoader 加载的。由于 BootstrapClassLoader 由 C++ 实现,由于这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的,所以拿到的结果是 null。

    public abstract class ClassLoader {
      ...
      // 父加载器
      private final ClassLoader parent;
      @CallerSensitive
      public final ClassLoader getParent() {
         //...
      }
      ...
    }
    

    下面是一个获取 ClassLoader 的示例:

    public class PrintClassLoaderTree {
    
        public static void main(String[] args) {
    
            ClassLoader classLoader = PrintClassLoaderTree.class.getClassLoader();
    
            StringBuilder split = new StringBuilder("|--");
            boolean needContinue = true;
            while (needContinue){
                System.out.println(split.toString() + classLoader);
                if(classLoader == null){
                    needContinue = false;
                }else{
                    classLoader = classLoader.getParent();
                    split.insert(0, "\t");
                }
            }
        }
    
    }
    

    输出结果:

    |--sun.misc.Launcher$AppClassLoader@18b4aac2
        |--sun.misc.Launcher$ExtClassLoader@53bd815b
            |--null
    

    可以看出:

    • 自定义编写的 Java 类 PrintClassLoaderTreeClassLoaderAppClassLoader
    • AppClassLoader 的父 ClassLoaderExtClassLoader
    • ExtClassLoader 的父 ClassLoaderBootstrap ClassLoader,因此输出结果为 null。

    自定义类加载器

    除了 BootstrapClassLoader 其他类加载器均由 Java 实现且全部继承自java.lang.ClassLoader。如果我们要自定义自己的类加载器,很明显需要继承 ClassLoader抽象类。

    ClassLoader 类有两个关键的方法:

    • protected Class loadClass(String name, boolean resolve):加载指定二进制名称的类,实现了双亲委派机制。name 为类的二进制名称,resolve 如果为 true,在加载时调用 resolveClass(Class c) 方法解析该类。

    • protected Class findClass(String name):根据类的二进制名称来查找类,默认实现是空方法。

    官方 API 文档中写到:

    建议 ClassLoader的子类重写 findClass(String name)方法而不是loadClass(String name, boolean resolve) 方法。

    如果我们不想打破双亲委派模型,就需要重写 ClassLoader 类中的 findClass() 方法,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass() 方法。

    实现自定义类加载器

    以下是我们自行实现自定义类加载器的一个示例:

    import java.io.*;
    
    public class CustomClassLoader extends ClassLoader {
    
        private String pathToBin;
    
        public CustomClassLoader(String pathToBin) {
            this.pathToBin = pathToBin;
        }
    
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            try {
                byte[] classData = loadClassData(name);
                return defineClass(name, classData, 0, classData.length);
            } catch (IOException e) {
                throw new ClassNotFoundException("Class " + name + " not found", e);
            }
        }
    
        private byte[] loadClassData(String name) throws IOException {
            String file = pathToBin + name.replace('.', File.separatorChar) + ".class";
            InputStream is = new FileInputStream(file);
            ByteArrayOutputStream byteSt = new ByteArrayOutputStream();
            int len = 0;
            while ((len = is.read()) != -1) {
                byteSt.write(len);
            }
            return byteSt.toByteArray();
        }
    }
    

    示例说明:

    • 构造器:接受一个字符串参数,这个字符串指定了类文件的存放路径。
    • 覆写 findClass 方法:当父类加载器无法加载类时,findClass 方法会被调用。在这个方法中,首先使用 loadClassData 方法读取类文件的字节码,然后调用 defineClass 方法来将这些字节码转换为 Class 对象。
    • loadClassData 方法:读取指定路径下的类文件内容,并将内容作为字节数组返回。

    类加载器加载规则

    JVM 启动的时候,并不会一次性加载所有的类,而是根据需要去动态加载。也就是说,大部分类在具体用到的时候才会去加载,这样对内存更加友好。

    对于已经加载的类会被放在 ClassLoader 中。在类加载的时候,系统会首先判断当前类是否被加载过。已经被加载的类会直接返回,否则才会尝试加载。也就是说,对于一个类加载器来说,相同二进制名称的类只会被加载一次。

    public abstract class ClassLoader {
      ...
      private final ClassLoader parent;
      // 由这个类加载器加载的类。
      private final Vector<Class<?>> classes = new Vector<>();
      // 由 JVM 调用,用此类加载器记录每个已加载类。
      void addClass(Class<?> c) {
            classes.addElement(c);
       }
      ...
    }
    

    类加载器工作过程

    类加载器(Class Loader)在 Java 虚拟机(JVM)中的工作过程是一个复杂而精细的流程。类加载器不仅负责加载类的字节码文件,还要确保类的正确性和初始化。

    JVM(Java虚拟机)——类的生命周期与加载过程

    类加载器的工作过程可以分为以下几个主要阶段:

    1. 加载(Loading):在加载阶段,类加载器负责读取类的二进制数据,并将其转化为 Class 对象。这一阶段包括以下几个步骤:
    • 查找或获取类的二进制数据:类加载器会根据类的全限定名(例如 com.example.MyClass)查找并加载类的字节码文件。
    • 生成 Class 对象:类加载器将字节码文件转化为 Class 对象,并存放在方法区中。
    1. 验证(Verification):验证阶段是为了确保类文件的字节码符合 Java 虚拟机的规范,防止恶意代码危害虚拟机。验证阶段主要包括以下几个子阶段:

      • 文件格式验证:确保字节流的格式符合 Class 文件格式规范。
      • 元数据验证:确保类的元数据信息(如常量池中的常量)正确无误。
      • 字节码验证:确保字节码指令符合 JVM 规范,不会导致非法操作。
      • 符号引用验证:确保符号引用能正确解析到实际存在的类、接口、方法或字段。
    2. 准备(Preparation):准备阶段主要是为类变量分配内存空间,并设置类变量的初始值。注意,这里的类变量指的是被 static 修饰的变量。实例变量则是在对象实例化时分配内存空间。

    3. 解析(Resolution):解析阶段是将符号引用转换为直接引用的过程。符号引用指的是类名、接口名、方法名等字符串形式的引用,而直接引用则指向目标对象在内存中的地址。

    4. 初始化(Initialization):初始化阶段是执行类构造器 () 方法的过程。在这个阶段,类中的静态变量会被赋予初始值,并执行静态块中的代码。初始化阶段还包括类中非静态方法的调用和类的实例化。

  • 相关阅读:
    Git入门(保姆级教学)
    RocketMQ这样做,压测后性能提高30%
    正整数分解(c++基础)
    矩阵股份上市首日跌破发行价:振幅达10%,王冠为实际控制人
    温故知新(十一)——IIC
    kubernetes popeye 巡检
    vue 监听dom元素尺寸大小改变
    计算机组成原理——第一章
    信息系统项目管理师第四版学习笔记——项目进度管理
    APA泊车名词解释
  • 原文地址:https://blog.csdn.net/Zachyy/article/details/142290797