带你看看 JVM 的类加载器

本文详细探讨了JVM中类加载器的工作原理、生命周期、分类(包括Bootstrap、Extension和Application Classloader)、双亲委派机制及其重要性,以及如何破坏并合理利用委派。特别关注了自定义类加载器在特定场景下的应用,如JDBC打破双亲委派的案例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM 类加载器

一、前言

对于校招来说,jvm 的相关知识总是处在一个尴尬的境地,想要彻底了解jvm的知识,就需要阅读大量的文档,面试的时候,这些知识也就只会浓缩成短短几句话,而且对于学生时代遇到的中小型项目,一般到不来优化 JVM 的地步,也就无从谈起 JVM 实战了,没有实战,之前学习的知识很容易就遗忘了。

所以说,目前 JVM 最好的学习方式,是先通过阅读文档了解 JVM 的各个部分及其运作方式,然后总结成精量化的博客,后期可以通过不断回顾博客加强记忆。(成为文科工程师也没有办法,不这么做别人就把你卷死)

所以,这篇博客是我通过阅读大量文档总结成的册子,我会尽量以问答的形式编写,主要是为了方便应付面试,不建议拿来做 JVM 的入门。想要入门 JVM ,可以去翻阅下面这篇学习笔记:

https://www.pdai.tech/md/java/jvm/java-jvm-classload.html#%E7%B1%BB%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F

本篇主要涉及的内容是类加载器相关的知识,其在 JVM 中的位置,我也在下图标出了:

类加载器在 JVM 中的位置

二、类加载器知识点整理

1、谈谈类的生命周期

类的生命周期分为这几个步骤:

  • 加载:类加载器加载的,是字节码文件,我们写的 .java 文件,会通过编译器,编译为 .class 文件,即字节码文件。加载的时候,先是使用类的全限定名,获取字节码文件,然后将字节码文件表示的静态结构转换为方法区的运行时数据结构,最后,在堆区生成 对应的 java.lang.Class 对象

加载

  • 连接:

连接分为下面几个字部分:

**验证:**验证加载的字节码文件是否合规,确保加载的字节码文件不会破坏虚拟机的安全性

**准备:**这阶段,为 static 变量分配内存空间,并将其初始化为 0 (其具体的数值,是在初始化阶段进行的)

**解析:**将符号引用转换为直接引用(符号引用是用字符表示引用的目标;直接引用就是将符号引用定位到目标了<这就要求一定要加载到内存中>)

  • 初始化:

初始化,即对类的静态变量进行初始化,在连接准备阶段,会将静态变量,先赋予 0 的初值,这里的初始化就是将我们自己定义的值附上去

  • 使用:

类访问方法区内的数据结构的接口, 对象是Heap区的数据。

  • 卸载:

jvm 生命周期结束的情况如下:

1、执行 System.exit()

2、程序正常执行完毕

3、程序出现异常或错误

4、因为 OS 错误导致 JVM 进程结束

2、谈谈 JVM 类加载器的分类

类加载器可以分为下面三类:

  • BootstrapClassloader:

所有以 java.* 开头的文件,都会由其加载

其是使用 c++ 编写的,我们没法直接获取到

  • ExtensionCLassLoader:

从java.ext.dirs加载,或从jdk的安装目录的jre/lib/ext子目录(扩展目录)下加载类库,如果程序员将jar包导入到这些目录中,扩展类加载器也会进行加载

  • AppliactionCLassloader:

加载我们自己写的 .java

(这里要注意,虽然我们有父子类加载器的称呼,但子类加载器并不是使用继承的方式继承父加载器的,而是使用组合的方式,即子加载器有一个父加载器的属性,其关系可以参考 controller 中调用 service)

3、双亲委派的过程

  • AppClassLoader 加载时,先尝试交给 ExtCLassLoader 加载
  • ExtCLassLoader 加载时,先尝试交给 BootStrapClassLoader 加载
  • BootstrapClassLoader 加载失败,再交给 ExtCLassLoader 加载
  • ExtCLassLoader 加载失败,再交给 AppClassLoader 进行加载

总的来说,就是先往上踢皮球,上面解决不了,自己再尝试加载

4、为什么需要双亲委派

1、防止系统中出现多分同样的字节码(举例:我们自己写一个 java.lang.String,不会被加载)

2、保证 java 程序安全稳定的运行(假设通过网路传来一个 java.lang.String 的类,通过双亲委派传到了 BootstrapClassLoader ,这个时候启动类加载器发现这个类已经被加载过了,就不会去加载这个网络传来的类,从而保证核心类库不会被篡改)

5、如何破坏双亲委派机制

继承 ClassLoader 类,重写 findClass() 方法

package top.faroz.jvm.classloader;
import java.io.*;

public class MyClassLoader extends ClassLoader {

  	// 加载文件的路径
    private String root;

  	// 重写 findCLass 方法
  	// 在原来的 findClass 方法中,会先查询类有没有被加载
  	// 如果没有加载的话,就会先交给父类加载器进行加载
  	// 我们重写 findClass 方法的话,就完全不会去鸟父类加载器了,直接走自己
    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 = root + File.separatorChar
                + className.replace('.', File.separatorChar) + ".class";
      	// 读取对应的字节码文件
        try {
            InputStream ins = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 1024;
            byte[] buffer = new byte[bufferSize];
            int length = 0;
            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args)  {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("D:\\temp");

        Class<?> testClass = null;
        try {
            testClass = classLoader.loadClass("top.faroz.jvm.classloader.Test2");
            Object object = testClass.newInstance();s
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

6、为什么需要破坏双亲委派?有哪些框架破坏了双亲委派?

为什么:

有的时候,我们加载的字节码文件,可能需要通过网络传输,或者这些字节码文件被进行了加密,这个时候,默认的类加载器就无法加载这些字节码文件,就需要我们自定义类加载器来实现类加载的功能

有那些框架:

JDBC 就是通过破坏双亲委派机制

我们知道,JDBC 是由 java 提供接口,然后由不同的数据库厂商进行实现的

那MySQL举例,DriverManager 类中要加载各个实现了Driver接口的类,然后进行管理,但是DriverManager位于 $JAVA_HOME中jre/lib/rt.jar 包,由BootStrap类加载器加载,而其Driver接口的实现类是位于服务商提供的 Jar 包

又因为**类加载器有如下机制:**当被装载的类引用其他类的时候,虚拟机就会用装载第一个类的类加载器,去装载被引用的类

拿到 JDBC 中,就是会尝试用 BootStrapClassloader 去装载由第三方厂商实现的类,这显然是不能成功的,这里我们必须使用子类加载器去加载,所以破坏了双亲委派模型

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

FARO_Z

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值