【JVM】类加载器ClassLoader

一、简介

在Java中,类加载器(ClassLoader)是一个关键的组件,它负责将字节码文件加载到内存并转换成Java类。Java的类加载器主要可以分成两类:系统提供的和由Java应用开发人员编写的。Java开发者可以根据需要创建自己的类加载器。所有的类加载器都继承自抽象类ClassLoader。当JVM需要加载一个类时,它会首先请求父类加载器去尝试加载这个类,如果父类加载器无法找到相应的类或者该类的字节码文件,那么该请求就会传递给子类加载器,依此类推,直到某个类加载器找到了相应的字节码文件为止。

总的来说,Java中的类加载器是一个重要的环节,它确保了Java程序能够正确地运行和使用各种不同类型的类。

二、加载器分类

从JVM的角度看,类加载器可以分为两种:

  • 引导类加载器(启动类加载器 Bootstrap ClassLoader);

  • 其他所有类加载器,这些类加载器由 java 语言实现,独立存在于虚拟机外部,并且全部继承自抽象类 java.lang.ClassLoader。

XML 复制代码
- 引导类加载器
  - 扩展类加载器(Extension ClassLoader)
    - 应用程序类加载器(Application ClassLoader)
      - 自定义类加载器

从 java 开发人员的角度来看,类加载器就应当划分得更细致一些,自 JDK1.2 以来 java 一直保持者三层类加载器:

1. 引导类加载器(启动类加载器 BootStrap ClassLoader)

  • 这个类加载器使用 C/C++语言实现,嵌套在 JVM 内部.它用来加载 java 核心类库.并不继承于 java.lang.ClassLoader 没有父加载器。

  • 负责加载扩展类加载器和应用类加载器,并为他们指定父类加载器。

  • 出于安全考虑,引用类加载器只加载存放在<JAVA_HOME>\lib 目录,或者被-Xbootclasspath 参数锁指定的路径中存储放的类。

2. 扩展类加载器(Extension ClassLoader)

  • Java 语言编写的,由 sun.misc.Launcher$ExtClassLoader 实现;

  • 派生于 ClassLoader 类;

  • 从 java.ext.dirs 系统属性所指定的目录中加载类库,或从 JDK 系统安装目录的jre/lib/ext 子目录(扩展目录)下加载类库.如果用户创建的 jar 放在此目录下,也会自动由扩展类加载器加载。

3. 应用程序类加载器(系统类加载器 Application ClassLoader)

  • Java 语言编写的,由 sun.misc.Launcher$AppClassLoader 实现;

  • 派生于 ClassLoader 类;

  • 加载我们自己定义的类,用于加载用户类路径(classpath)上所有的类;

  • 该类加载器是程序中默认的类加载器;

  • ClassLoader 类 , 它 是 一 个 抽 象 类 , 其 后 所 有 的 类 加 载 器 都 继 承 自 ClassLoader(不包括启动类加载器)。

自定义类加载器的实现通常继承自java.lang.ClassLoader类或其子类。以下是一个简单的自定义类加载器示例:

java 复制代码
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;

public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = getFileName(className);
        try (InputStream is = getParent().getResourceAsStream(fileName)) {
            if (is == null) {
                return null;
            }
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = is.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

在这个示例中,我们创建了一个名为CustomClassLoader的自定义类加载器,它继承自ClassLoader类。我们重写了findClass方法,该方法接收一个字符串参数name,表示要加载的类的全名。在这个方法中,我们首先调用loadClassData方法来读取类的字节码数据,然后使用defineClass方法将字节码数据转换为Java类。

三、类加载过程

1. 加载

  • 通过类名(地址)获取此类的二进制字节流。

  • 将这个字节流所代表的静态存储结构转换为方法区(元空间)的运行时结构。

  • 在内存中生成一个代表这个类的java.lang.class对象,作为这个类的各种数据的访问入口。

2. 链接

  • **验证:**检验被加载的类是否有正确的内部结构,并和其他类协调一致;

    验证文件格式是否一致: class 文件在文件开头有特定的文件标识(字节码文件都以 CA FE BA BE 标识开头);主,次版本号是否在当前 java 虚拟机接收范围内;

    元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合java 语言规范的要求,例如这个类是否有父类;是否继承浏览不允许被继承的类(final 修饰的类).....

  • **准备:**准备阶段则负责为类的静态属性分配内存,并设置默认初始值;

    不包含用 final 修饰的 static 常量,在编译时进行初始化;

    例如: public static int value = 123;value 在准备阶段后的初始值是 0,而不是 123;

  • **解析:**将类的二进制数据中的符号引用替换成直接引用(符号引用是 Class 文件的逻辑符号,直接引用指向的方法区中某一个地址)。

3. 初始化

初始化,为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化。初始化阶段就是执行底层类构造器方法<clinit>()的过程。此方法不需要定义,是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来的。

类什么时候初始化:

JVM规定:每个类或者接口被首次主动使用时才对其进行初始化。

  • 通过 new 关键字创建对象;

  • 访问类的静态变量,包括读取和更新;

  • 访问类的静态方法;

  • 对某个类进行反射操作;

  • 初始化子类会导致父类的的初始化;

  • 执行该类的 main 函数;

除了以上几种主动使用,以下情况被动使用,不会加载类:

  • 引用该类的静态常量,注意是常量,不会导致初始化,但是也有意外,这里的常量是指已经指定字面量的常量,对于那些需要一些计算才能得出结果的常量就会导致类加载,比如:

    public final static int NUMBER = 5 ; //不会导致类初始化,被动使用

    public final static int RANDOM = new Random().nextInt() ; //会导致类加载。

  • 构造某个类的数组时不会导致该类的初始化,比如:

    Student[] students = new Student[10] ;

类的初始化顺序

  • 对static修饰的变量或语句块进行赋值

    如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行;

    如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类;

    顺序是:父类 static --> 子类 static。

四、双亲委派机制

Java 虚拟机对 class 文件采用的是按需加载的方式,也就是说当需要该类时才会将它的 class 文件加载到内存中生成 class 对象。而且加载某个类的 class 文件时,Java 虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。

工作原理:

  • 如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请 求委托给父类的加载器去执行;

  • 如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器。

  • 如果父类加载器可以完成类的加载任务,就成功返回,倘若父类加载器无法完成加载任务,子加载器才会尝试自己去加载,这就是双亲委派机制.

  • 如果均加载失败,就会抛出 ClassNotFoundException 异常。

双亲委派优点:

  • 安全,可避免用户自己编写的类替换 Java 的核心类,如 java.lang.String;

  • 避免类重复加载,当父亲已经加载了该类时,就没有必要子 ClassLoader 再加载一次。

如何打破双亲委派机制

Java 虚拟机的类加载器本身可以满足加载的要求,但是也允许开发者自定义类加载器。

在 ClassLoader 类中涉及类加载的方法有两个,loadClass(String name), findClass(String name),这两个方法并没有被 final 修饰,也就表示其他子类可以重写。

  • 重写 loadClass 方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制, 不推荐)。

  • 重写 findClass 方法 (推荐)。

我们可以通过自定义类加载重写方法打破双亲委派机制, 再例如 tomcat 等都有自己定义的类加载器。

java 复制代码
public class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] b = loadClassData(name);
        if (b == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, b, 0, b.length);
        }
    }

    private byte[] loadClassData(String className) {
        // 这里可以根据实际情况从文件、网络等地方读取字节码数据
        // 为了简化示例,我们直接返回null
        return null;
    }
}

在这个示例中,MyClassLoader继承自ClassLoader,并重写了findClass方法。当JVM需要加载一个类时,它会首先请求父类加载器去尝试加载这个类,如果父类加载器无法找到相应的类或者该类的字节码文件,那么该请求就会传递给MyClassLoader,然后调用MyClassLoader的findClass方法。在findClass方法中,我们首先调用loadClassData方法来获取类的字节码数据,然后将这些字节码数据转换成Java类。

相关推荐
YAy1728 分钟前
CC3学习记录
java·开发语言·学习·网络安全·安全威胁分析
代码小鑫30 分钟前
A035-基于Spring Boot的企业内管信息化系统
java·开发语言·spring boot·后端·spring
qq_35323353892 小时前
【原创】java+ssm+mysql校园疫情防控管理系统设计与实现
java·mvc·javaweb·ssm框架·bs·疫情防控
代码调试4 小时前
Springboot校园失物招领平台
java·spring boot
camellias_5 小时前
SpringBoot(二十三)SpringBoot集成JWT
java·spring boot·后端
tebukaopu1485 小时前
springboot如何获取控制层get和Post入参
java·spring boot·后端
昔我往昔5 小时前
SpringBoot 创建对象常见的几种方式
java·spring boot·后端
q567315236 小时前
用 PHP或Python加密字符串,用iOS解密
java·python·ios·缓存·php·命令模式
灭掉c与java6 小时前
第三章springboot数据访问
java·spring boot·后端
啊松同学6 小时前
【Java】设计模式——工厂模式
java·后端·设计模式