JVM(二)——类的加载机制
一、类加载的过程
1.1类加载器
![![[Pasted image 20251229163424.png]]](https://i-blog.csdnimg.cn/direct/2675f4e6daf74d73984698f21b87d92a.png)
1.2类加载过程
![![[Pasted image 20251229163542.png]]](https://i-blog.csdnimg.cn/direct/c7fae6a59f074b9ca1f313b497c5b896.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文件从磁盘加载到内存。在写程序的时候,我们几乎是不需要关心类的加载的,因为这些都是隐式加载的。类的装载方式有两种:
- 隐式装载:在使用new来创建对象的时候,就会隐式调用类装载器加载对应的类到jvm里面
- 显式装载:反射的时候,需要显式调用类装载器
3.JVM 类加载机制
- 加载
- 验证:文件格式,字节码,符号引用,元数据
- 准备:静态变量分配内存,默认值
- 解析:符号引用转换为直接引用
- 初始化:构造方法,静态变量赋初值,静态代码块
4.什么是类加载器,类加载器有哪些?
通过类的全限定名找到对应的.class二进制字节流的代码块。
启动类,扩展类,应用程序类,自定义
5.说一下类加载的执行过程?
加载->验证->准备->解析->初始化
6.什么是双亲委派模型?
当一个类加载器收到加载类的请求的额时候,不会立即加载类,而是委派给他的父类加载器,父类加载器又会委派给他的父类加载器,直到找到启动类加载器,如果父类加载器可以加载到类就直接返回,否则再自己加载。
- 类的唯一性和安全性
- 防止类复用,减少内存消耗
7.简单说说你了解的类加载器,可以打破双亲委派么,怎么打破。
打破:继承ClassLoader,而且还要重写loadClass和findClass方法
8.什么是Java虚拟机?为什么Java被称作是“平台无关的编程语言”?
Java被编译之后会生成字节码文件,jvm就是把字节码文件转换为系统能够执行的机器指令。这就使得,只要你的系统有相应版本的jvm,就可以执行字节码,不需要程序员单独重写。
更多推荐




所有评论(0)