一、类加载的过程

1.1类加载器

![[Pasted image 20251229163424.png]]

1.2类加载过程

![[Pasted image 20251229163542.png]]

加载:

通过类的全限定名(包名+类名),获取到该类的.class文件的二进制字节流,将二进制字节流所代表的静态存储结构,转化为方法区运行时的数据结构,在内存中生成一个代表该类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口

连接:验证、准备、解析3个阶段统称为连接。

验证:确保class文件中的字节流包含的信息,符合当前虚拟机的要求,保证这个被加载的class类的正确性,不会危害到虚拟机的安全。验证阶段大致会完成以下四个阶段的检验动作:文件格式校验、元数据验证、字节码验证、符号引用验证
准备:为类中的静态字段分配内存,并设置默认的初始值(0/null/false),比如int类型初始值是0。被final修饰的static字段不会设置,因为final在编译的时候就分配了
解析:解析阶段是虚拟机将常量池的「符号引用」直接替换为「直接引用」的过程。符号引用是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用的时候可以无歧义地定位到目标即可。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,直接引用是和虚拟机实现的内存布局相关的。如果有了直接引用,那引用的目标必定已经存在在内存中了。

初始化:

初始化是整个类加载过程的最后一个阶段,初始化阶段简单来说就是执行类的构造器方法,这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑

使用:使用类或者创建对象
卸载:

一个类要被JVM卸载,条件非常苛刻,需要同时满足以下三点:

  • 该类所有的实例都已经被回收:这是最显而易见的前提。如果堆中还存在这个类的任何个实例对象,那么定义这个对象的Class对象肯定不能被卸载。
  • 加载该类的ClassLoader已经被回收:这是最关键也是最难满足的条件。类与加载器是双向绑定的共生关系。一个类由哪个类加载器加载,这个信息是存储在Class对象里的。要卸载一个类,必须先卸载加载它的类加载器。
  • 类对应的Java.lang.Class对象没有任何地方被引用:不能在任何地方通过反射(如静态字段、全局变量)、静态变量、JNI等途径引用到这个Class对象。一旦这个Class对象还存在强引用,GC就不会回收它,那么这个类也就不会被卸载。

1.3双亲委派模型

当某个类加载器(比如自定义加载器)收到加载类的请求时,它不会先自己动手,而是把请求“委派”给父加载器;父加载器同样会继续委派给它的父加载器,直到传到最顶层的启动类加载器。如果父加载器能找到并加载这个类,就直接返回;如果所有父加载器都加载不了(比如不在它们的加载范围内),子加载器才会自己去尝试加载。
这种机制的核心作用有两个:

  • 保证类的唯一性和安全性:避免同一个类被不同加载器重复加载,确保核心类(如JDK的string、Integer)不会被篡改。比如上面的例子,防止我们自定义的string类替换掉JDK的核心类,否则可能引发安全问题(比如修改string的底层实现导致系统混乱)。
  • 实现类的复用:核心类只需要被顶层加载器加载一次,所有子加载器都能共享这个类,减少内存消耗。

1.4如何破坏双亲委派

方式 1:重写 ClassLoader 的 loadClass 方法(最直接)
import java.io.FileInputStream;  
import java.io.IOException;  
import java.nio.ByteBuffer;  
import java.nio.channels.FileChannel;  
  
/**  
 * 自定义类加载器,重写loadClass方法,破坏双亲委派模型  
 * 核心:先尝试自己加载类,加载不到再委派父类加载器  
 */  
public class CustomClassLoader extends ClassLoader {  
    // 自定义类加载的根路径(存放要加载的.class文件)  
    private String classPath;  
  
    public CustomClassLoader(String classPath) {  
        // 父类加载器默认使用系统类加载器(Application ClassLoader)  
        this.classPath = classPath;  
    }  
  
    // 核心:重写loadClass方法,改变加载顺序  
    @Override  
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {  
        synchronized (getClassLoadingLock(name)) {  
            // 1. 检查该类是否已经被加载过  
            Class<?> c = findLoadedClass(name);  
            if (c == null) {  
                // 【破坏双亲委派的关键】先尝试自己加载,而不是先委派父类  
                try {  
                    c = findClass(name); // 自己加载  
                } catch (ClassNotFoundException e) {  
                    // 自己加载失败,再委派父类加载器  
                    c = super.getParent().loadClass(name);  
                }  
            }  
            if (resolve) {  
                resolveClass(c);  
            }  
            return c;  
        }  
    }  
  
    // 自定义类加载的核心逻辑:从指定路径读取.class文件并转为Class对象  
    @Override  
    protected Class<?> findClass(String name) throws ClassNotFoundException {  
        try {  
            // 将类名转为文件路径(如com.example.Test -> com/example/Test.class)  
            String filePath = classPath + "/" + name.replace(".", "/") + ".class";  
            FileInputStream fis = new FileInputStream(filePath);  
            FileChannel channel = fis.getChannel();  
            ByteBuffer buffer = ByteBuffer.allocate((int) channel.size());  
            channel.read(buffer);  
            fis.close();  
            // 将字节数组转为Class对象  
            return defineClass(name, buffer.array(), 0, buffer.array().length);  
        } catch (IOException e) {  
            throw new ClassNotFoundException("类加载失败:" + name, e);  
        }  
    }  
  
    // 测试方法  
    public static void main(String[] args) throws Exception {  
        // 1. 创建自定义类加载器,指定类加载路径(替换为你自己的.class文件路径)  
        CustomClassLoader customLoader = new CustomClassLoader("D:/test/classpath");  
        // 2. 加载自定义类(假设D:/test/classpath下有com/example/Test.class)  
        Class<?> testClass = customLoader.loadClass("com.example.Test");  
        // 3. 输出类加载器信息,验证是自定义加载器加载的(而非父类)  
        System.out.println("类加载器:" + testClass.getClassLoader());  
        // 4. 实例化并调用方法(验证类加载成功)  
        Object obj = testClass.newInstance();  
        testClass.getMethod("sayHello").invoke(obj);  
    }  
}
  • 核心修改点:重写loadClass方法,把 “先委派父类” 改成 “先自己加载,失败再委派父类”,这是对双亲委派最直接的破坏。
  • findClass方法:实现自定义的类加载逻辑(从指定路径读取.class 文件字节码,再通过defineClass转为 Class 对象),这是自定义类加载器的标准写法。
  • 前置条件:需要在D:/test/classpath下放置编译后的.class文件(如com/example/Test.class),否则会触发父类加载器加载。
方式 2:使用线程上下文类加载器(SPI 场景的间接破坏)

本质是让父类加载器(如启动类加载器)通过线程上下文类加载器,间接调用子类加载器(应用类加载器)加载类,变相破坏双亲委派。

二、面试题整理

1.简述java类加载机制?

加载->验证->准备->解析->初始化

2.描述一下JVM加载Class文件的原理机制

所有的类只有类加载器装载到jvm里面的时候才能运行。类加载器本身也是一个类,他的工作就是把class文件从磁盘加载到内存。在写程序的时候,我们几乎是不需要关心类的加载的,因为这些都是隐式加载的。类的装载方式有两种:

  1. 隐式装载:在使用new来创建对象的时候,就会隐式调用类装载器加载对应的类到jvm里面
  2. 显式装载:反射的时候,需要显式调用类装载器

3.JVM 类加载机制

  1. 加载
  2. 验证:文件格式,字节码,符号引用,元数据
  3. 准备:静态变量分配内存,默认值
  4. 解析:符号引用转换为直接引用
  5. 初始化:构造方法,静态变量赋初值,静态代码块

4.什么是类加载器,类加载器有哪些?

通过类的全限定名找到对应的.class二进制字节流的代码块。
启动类,扩展类,应用程序类,自定义

5.说一下类加载的执行过程?

加载->验证->准备->解析->初始化

6.什么是双亲委派模型?

当一个类加载器收到加载类的请求的额时候,不会立即加载类,而是委派给他的父类加载器,父类加载器又会委派给他的父类加载器,直到找到启动类加载器,如果父类加载器可以加载到类就直接返回,否则再自己加载。

  1. 类的唯一性和安全性
  2. 防止类复用,减少内存消耗

7.简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。

打破:继承ClassLoader,而且还要重写loadClass和findClass方法

8.什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?

Java被编译之后会生成字节码文件,jvm就是把字节码文件转换为系统能够执行的机器指令。这就使得,只要你的系统有相应版本的jvm,就可以执行字节码,不需要程序员单独重写。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐