这篇文章中我们介绍类加载过程的几个阶段,分为加载、验证、准备、解析、初始化等几个阶段,包括这几个阶段都执行了哪些动作,还介绍了类加载器的基本流程和关键概念,包括双亲委派模型、自定义类加载器以及类的生命周期。
一、类加载生命周期
- 介绍
- 类加载的生命周期过程分类五个阶段:加载、验证、准备、解析、初始化;类的生命周期还包括使用、卸载;其中这个过程遵循双亲委派机制;我们文章后边讲解该机制;
- 类加载的生命周期过程分类五个阶段:加载、验证、准备、解析、初始化;类的生命周期还包括使用、卸载;其中这个过程遵循双亲委派机制;我们文章后边讲解该机制;
- 下边分别介绍一下上图中的这几部分
- 加载: 将java文件的 .class文件(也就是字节码文件)加载到jvm虚拟机中。
- 验证: 验证该.class文件(字节码文件)的正确性
- 准备: 给静态变量进行分配内存。并赋值一些默认值,比如int 赋值 0 boolean赋值 false;
- 解析: 就是把常量池中的符号引用转化为直接引用,指向内存中的指针或地址;解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行,此类属于静态链接;动态链接是指在程序运行期间完成的一些地址引用和创建,即用到才加载,不用到不加载,称为动态链接。比如:多态,在程序运行期间才确定用的是哪个子类,才会进行将符号引用转换为直接引用。
- 初始化: 将静态变量改为指定值,并执行静态代码块。
- 假如这个类还没有被加载和连接,则程序先加载并连接该类
- 假如该类的直接父类还没有被初始化,则先初始化其直接父类
- 假如类中有初始化语句,则系统依次执行这些初始化语句
- 使用:就是我们程序在执行时对对象的操作就是使用。
- 卸载:当一个类不再被任何对象引用并且满足垃圾回收条件时,JVM可能会卸载这个类以释放内存资源。类卸载的过程通常非常复杂,因为它涉及到类加载器和垃圾回收器的交互。或者当程序结束;
二、类加载器
- 介绍
- 类加载是通过JVM中类加载器进行类加载的,一般使用到的类加载器有:引导类加载器、扩展类加载器、应用程序类加载器。当然我们也可以自定义类加载器。
- 引导类加载器: 用于加载一些jdk的核心包,比如 rt.jar、charsets.jar。引导类加载器是由C++实现的。
- 扩展类加载器: 用于加载一些jdk目录下ext目录下的一些扩展jar包;它的parent字段是null 也就是引导类加载器。因为引导类加载器是由c++实现的。
- 应用程序类加载器: 用于加载我们自己编写的一些类文件信息。也就是我们的项目target目录下编译后的一些.class文件。它的parent字段为扩展类加载器的实例。
- 自定义加载器: 需要继承ClassLoader类,然后重写它的一些方法(findClass 和 loadClass)加载我们自定义的内容。
- 类加载是通过JVM中类加载器进行类加载的,一般使用到的类加载器有:引导类加载器、扩展类加载器、应用程序类加载器。当然我们也可以自定义类加载器。
- 下边是一个简单的自定义类加载器示例且实现了双亲委派机制
import java.io.InputStream;
import java.lang.reflect.Method;
public class CustomClassLoader extends ClassLoader {
/** 存放类文件的根目录 */
private final String rootDir;
public CustomClassLoader(String rootDir) {
super(); // 调用父类构造函数,使自定义类加载器能够遵循双亲委派模型
this.rootDir = rootDir;
}
public static void main(String[] args) throws Exception {
CustomClassLoader loader = new CustomClassLoader("E:\\JVM");
Class<?> clazz = loader.loadClass("com.xxx.xxx.MyTestClass");
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("test");
method.invoke(instance);
}
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(className)) {
// 先从缓存中查找已加载的类
Class<?> clazz = findLoadedClass(className);
if (clazz == null) {
try {
// 尝试使用父类加载器来加载类,注意此处parent会有为null的情况
// 因为引导类加载器是没有parent的
clazz = getParent().loadClass(className);
} catch (ClassNotFoundException e) {
System.err.println("未加载到类");
}
if (clazz == null){
// 如果父类加载器无法加载,则尝试使用自定义类加载器来加载
clazz = findClass(className);
}
}
if (resolve) {
resolveClass(clazz); // 链接(链接)类
}
return clazz;
}
}
@Override
protected Class<?> findClass(String className) throws ClassNotFoundException {
byte[] classData = getClassData(className);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(className, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
String path = rootDir + "/" + className.replace('.', '/') + ".class";
try (InputStream is = getClass().getResourceAsStream(path)) {
if (is != null) {
int size = is.available();
byte[] buffer = new byte[size];
is.read(buffer);
return buffer;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}
}
三、类加载机制
- 介绍
- 类加载机制有全盘委托机制、父类委托机制、缓存机制及目前jvm中使用的双亲委派机制;
- 全盘委托机制:
- 当加载一个类的时候,如果需要加载其他类,则会使用同一个类加载器去加载,如果没有再进行双亲委派进行寻找。(是指当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类 所依赖及引用的类也由这个ClassLoder载入。)
- 父类委托机制
- 先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。
- 缓存机制
- 缓存机制会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。
- 双亲委派机制过程
- 依次从下往上一次委托父加载器来寻找已加载的类中(即findLoadClass)是否有要寻找的类,当找到顶层加载器中还是没有,则再一级一级往下去加载类寻找,执行findClass方法从指定的目录中寻找;(引导类加载器加载的是 lib包目录下的文件,扩展类加载的是 ext/下的jar文件。应用程序类加载器寻找的是项目中target目录下的文件)。即
- 依次从下往上一次委托父加载器来寻找已加载的类中(即findLoadClass)是否有要寻找的类,当找到顶层加载器中还是没有,则再一级一级往下去加载类寻找,执行findClass方法从指定的目录中寻找;(引导类加载器加载的是 lib包目录下的文件,扩展类加载的是 ext/下的jar文件。应用程序类加载器寻找的是项目中target目录下的文件)。即
- 疑问
- Q:为什么要这样设计?
- A:为了避免类的重复加载。如果每次都需要从上依次往下找,那么 每次加载类的是否都需要走一遍所有的类加载器。
- Q:安全机制(沙箱安全机制)?
- A:防止核心类库被进行篡改,jdk自带的一些核心类库是不允许被修改的,即 如果我们自定义 类&包名为 java.lang.String 是不允许的。
- Q:怎么打破双亲委派?
- A:在我们自定义的类加载器中重写它的loadClass方法,因为双亲委派机制的主要实现就是在这里。
- 扩展:
- Tomcat打破双亲委派机制
- Tomcat中可以存放多个war包,每个war包下所引用到的所有类都是使用自己的,也会有共存区,如果使用双亲委派的话,那么 所有war包使用的都就是同一版本的类文件了。jsp再tomcat中 每一个jsp就是一个类加载器,实现热部署就是每次替换jsp的时候都会把对应的类加载器卸载掉 重新进行注册。