JVM-类加载器

本次目标

  1. 理解JVM是什么
  2. 能够描述JVM架构图
  3. 掌握JVM类加载器相关概念

JVM概念

JVMJava虚拟机的简称,是Java语言的核心组件,负责充当Java代码和底层操作系统之间的中间层。JVM的主要任务是将Java字节码转换为特定平台的机器码并执行。

JVM架构图

JVM采用了一种基于栈的架构,包括类加载器、运行时数据区、执行引擎和本地方法栈等组件。这些组件协同工作,实现了Java程序的运行和管理。

JVM类加载器

类加载器的概念

类加载器Class Loader是Java运行时环境中的一个重要组件,负责加载Java类和资源文件。它的主要功能是根据类的全限定名查找并加载对应的字节码,将其转换为可执行的 Java 。类加载器在Java虚拟机JVM中起着至关重要的作用,确保Java应用程序能够正确地加载和运行类。

类加载器的分类

  1. 启动类加载器 Bootstrap ClassLoader:负责加载Java核心库(如java.lang包)中的类。它主要加载JAVA_HOME\lib\rt.jar目录下的类库,或者通过-Xbootclasspath参数指定的路径中的类库。启动类加载器是用原生代码实现的,位于Java虚拟机的最顶层,没有父类加载器。
  2. 扩展类加载器 Extensions ClassLoader:负责加载Java扩展库(如javax包)中的类。扩展类加载器继承自启动类加载器,因此它的搜索路径包括Java核心库和所有已安装的扩展库。扩展类加载器加载JAVA_HOME\lib\ext目录下的类库,或者通过java.ext.dirs系统变量指定的路径中的类库。
  3. 应用程序类加载器 Application ClassLoader:负责加载用户路径classpath上的类。应用程序类加载器根据Java应用程序的类路径classpath加载类,搜索路径包括Java应用程序的类路径以及Java核心库和扩展库。
  4. 自定义类加载器 User ClassLoader:负责加载应用之外的类文件。

类加载器的特点

双亲委派机制

什么是双亲委派

当一个类加载器收到类加载任务,会先交给其父类加载器去完成,因此最终加载任务都会传递到顶层的启动类加载器,只有当父类加载器无法完成加载时,才会尝试执行加载任务

为什么需要双亲委派

  1. 避免类的重复加载:在Java虚拟机中,每个类加载器都有自己的命名空间,负责加载并维护一组类的定义。当一个类加载器收到加载类的请求时,它会先检查自己的命名空间中是否已经加载此类,如果已经加载,则直接返回已加载的类定义,避免了重复加载。如果没有加载,则将加载请求委派给父加载器,由父加载器继续尝试加载,这样一级一级向上委派,直到找到能够加载该类的加载器或者到达顶层的启动类加载器。这种机制确保了类只会被加载一次。避免了被重复加载的问题。
  2. 保证类的安全性 :由于父加载器会在委派给子加载器加载类之前尝试加载,意味着核心Java类库会由顶层的启动类加载器加载,而不会被其他加载器替换。可以防止恶意代码通过替换核心类库来执行一些危险操作,确保了Java类库的完整性和安全性。

双亲委派机制源码

scss 复制代码
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            //首先检查请求的类是否被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                    //如果没有被加载过,就进行加载操作
                long t0 = System.nanoTime();
                try {
                        //加载时,如果存在父-类加载器,就用父-类加载器加载
                        //如果没有父-类加载器,就说明这个类加载器是启动类加载器,就找启动类加载器进行加载
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                    //如果父-类加载器抛出异常,说明父-类加载无法完成加载工作
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    //在父-类加载器无法完成加载的时候,再调用本身的findClass方法来进行类加载
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

为什么需要破坏双亲委派

在实际应用中,双亲委派机制解决了Java基础类统一加载的问题,但是存在缺陷。JDK基础类作为典型的API被用户调用,但是也存在基础类调用用户代码的情况:

比如在使用JDBC时, 利用DriverManager.getConnection获取连接时,DriverManager是由根类加载器Bootstrap加载的,在加载DriverManager时,会执行其静态方法,加载初始驱动程序,也就是Driver接口的实现类;但是这些实现类基本都是第三方厂商提供的,根据双亲委派原则,第三方的类不可能被根类加载器加载。

如何破坏双亲委派

  1. 继承ClassLoader抽象类,重写loadClass方法,在这个方法可以自定义要加载的类使用的类加载器
  2. 使用线程上下文加载器,可以通过java.lang.Thread类的setContextClassLoader()方法来设置当前类使用的类加载器类型

JDBC打破双亲委派源码

TODO

缓存机制

JVM中的类加载器通常会使用缓存来存储已加载的类信息,如果某个类已经被其父类加载器加载过,那么子类加载器就不会再次加载该类,直接使用缓存中的类。

类加载器的工作原理

类加载的时机

  1. 主动引用时加载:当程序使用到某个类时,如果该类还未被加载、链接和初始化,则会触发类的加载过程。主动引用包括创建类的实例、访问类的静态变量、调用类的静态方法等操作。
  2. 被动引用时加载:当访问某个类的静态常量时,只有声明该常量的类会被加载,而不会触发该类的初始化。被动引用的几种情况包括访问类的静态常量、通过数组定义类的引用类型、引用类的子类等。
  3. 反射调用时加载 :使用Java反射机制来动态创建类的实例、访问类的成员等操作时,会触发类的加载。

类加载的生命周期

  1. 加载阶段:加载阶段是类加载过程的第一步,它负责查找类的字节码,并将其加载到内存中。在加载阶段,会执行以下操作:
    1. 通过类的全限定名获取类的字节码文件。
    2. 将字节码文件加载到内存中,并在方法区创建一个代表该类的Class对象。
    3. 解析类的字节码,生成虚拟机可以使用的数据结构。
  2. 链接阶段:链接阶段是类加载过程的第二步,它负责将已经加载的类与其他类和符号引用进行关联。链接阶段又可以细分为以下几个步骤:
    1. 验证:验证阶段确保加载的字节码符合Java虚拟机规范,包括检查字节码的结构、语义和安全性等。
    2. 准备:准备阶段为类的静态变量分配内存,并设置默认初始值。
    3. 解析:解析阶段将符号引用转换为直接引用,即将类、方法、字段等的符号引用解析为内存中的直接指针。
  3. 初始化阶段:初始化阶段是类加载过程的最后一步,它负责执行类的初始化代码,包括静态变量的赋值和静态代码块的执行。初始化阶段会按照程序的执行顺序依次执行,且只会执行一次。

综上所述,类加载的过程可以归纳为加载、链接和初始化三个阶段。其中,加载阶段将类的字节码加载到内存中,链接阶段将类与其他类和符号引用关联,初始化阶段执行类的初始化代码。这三个阶段的顺序是依次进行的,加载阶段首先发生,然后是链接阶段,最后是初始化阶段。

自定义类加载器

目标:自定义类加载器,加载指定路径在D盘下的lib文件夹下的类

步骤:

  1. 新建一个类Test.java
typescript 复制代码
package com.sn.knit.classloader;

public class Test {

    public static void main(String[] args) {

    }

    public void say() {
        System.out.println("Hello World");
    }
}
  1. 编译Test.java到指定lib目录
  2. 自定义类加载器UserClassLoader继承ClassLoader:重写findClass()defineClass()方法
java 复制代码
package com.sn.knit.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

public class UserClassLoader extends ClassLoader {

    private String classpath;

    public UserClassLoader(String classpath) {
        super(ClassLoader.getSystemClassLoader());
        this.classpath = classpath;
    }

    /**
     * 加载class文件
     *
     * @param className 类的全限名称,定位class文件
     * @return {@link Class}<{@link ?}> Class对象
     */
    @Override
    protected Class<?> findClass(String className) throws ClassNotFoundException {
        // 输入流,通过类的全限定名称加载文件到字节数组
        byte[] classData = new byte[0];
        try {
            classData = getData(className);
            if (classData != null) {
                // defineClass方法将字节数组数据转为字节码文件
                return defineClass(className, classData, 0, classData.length);
            }
        } catch (FileNotFoundException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return super.findClass(className);
    }

    private byte[] getData(String className) throws IOException {
        String path = classpath + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
        try (InputStream is = new FileInputStream(path)) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            byte[] buffer = new byte[2048];
            int len = 0;
            while ((len = is.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
            return bos.toByteArray();
        }
    }
}
  1. 测试自定义类加载器
java 复制代码
package com.sn.knit.classloader;

import java.lang.reflect.Method;

public class TestUserClassLoader {

    public static void main(String[] args) throws Exception {
        UserClassLoader userClassLoader = new UserClassLoader(
                "D:\File\lib");

        Class c = userClassLoader.loadClass("com.sn.knit.classloader.Test");

        if (c != null) {
            Object o = c.newInstance();
            Method m = c.getMethod("say");
            m.invoke(o);
            System.out.println(c.getClassLoader().toString());
        }
    }
}

类加载器在实际开发中的应用

  1. 动态模块加载:许多应用需要动态地加载类或模块,通过自定义类加载器可以实现在运行时动态加载和卸载模块,实现插件式的架构。
  2. 热部署:在某些场景下,需要在应用程序运行过程中更新程序代码而不需要停止整个应用程序。自定义类加载器可以用于实现热部署,动态加载新的类文件并且卸载旧的类文件,以实现更新代码而不中断服务的功能。
  3. Class文件加密保护:某些安全要求较高的应用场景需要保护Class文件的安全性,通过自定义类加载器可以对加载的Class文件进行解密和校验,以确保Class文件的安全加载。
  4. 特定类的加载扩展:有些应用需要在启动时提前加载某些类,以提高启动速度,自定义类加载器可以在启动时提前加载需要的类,以加快应用程序的启动速度。
  5. 扩展类加载机制:在一些特殊的应用场景下,可能需要定制化的类加载机制,通过自定义类加载器可以扩展类加载机制,实现更加灵活的加载策略。
相关推荐
怒放吧德德7 小时前
JUC从实战到源码:JMM总得认识一下吧
java·jvm·后端
向阳12188 小时前
JVM学习路径
java·jvm
救苦救难韩天尊11 小时前
《JVM第9课》垃圾回收器
jvm
快乐非自愿14 小时前
redisson内存泄漏问题排查
java·开发语言·jvm
菜菜-plus15 小时前
jvm垃圾回收算法
java·jvm·算法
恬淡虚无真气从之16 小时前
进程 线程 和go协程的区别
java·开发语言·jvm
刽子手发艺16 小时前
初识JVM、解释和运行、内存管理、即时编译
java·jvm·后端
小小小妮子~1 天前
深入理解Java虚拟机(JVM):从基础到实战
java·开发语言·jvm
Jason-河山1 天前
爱回收根关键字获取对应品牌的ID API 返回值深入解析
java·jvm·算法
程序猿阿伟2 天前
《揭秘观察者模式:作用与使用场景全解析》
java·开发语言·jvm