目录
类的加载机制
类加载器
双亲委派模型机制
为什么要有双亲委派机制
如何打破双亲委派机制
类的加载机制
类加载过程是由Java虚拟机的类加载子系统完成的,它负责将字节码加载到内存中,并进行链接和初始化操作。类加载过程是Java中非常重要的一部分,也是实现Java动态性和灵活性的基础。
有以下三个过程:
①加载(Loading):
加载阶段是指查找字节码并创建一个代表这个类的Class对象的过程。
类加载器负责加载类的字节码文件(通常是.class文件)到内存中,并为之创建一个java.lang.Class对象。
加载阶段的主要任务是通过类的全限定名在文件系统或网络中定位到类的字节码文件,并读取到JVM内存中。
②链接(Linking):
链接阶段又包括验证(Verification)、准备(Preparation)和解析(Resolution)三个子阶段。
验证阶段是确保被加载的类的字节码是合法、符合规范的过程,以防止恶意代码的引入。
准备阶段是为类的静态变量分配内存空间并初始化默认初始值(零值)的过程。
解析阶段是将常量池中的符号引用替换为直接引用的过程。例如将类或接口的全限定名转换为直接引用。
③初始化(Initialization):
初始化阶段是对类进行初始化的过程,包括执行类构造器<clinit>()方法的过程。
当初始化一个类时,如果该类有父类,则会先初始化父类,如果父类还有父类,则继续向上初始化,直到顶层的父类为止。
在初始化阶段,程序员也可以通过静态代码块或静态变量的赋值语句来自定义类的初始化过程。
在web安全领域值得一提的是:
Class.forName("类名")默认会初始化被加载类的静态属性和方法,因此可以注入恶意代码
而ClassLoader.loadClass默认不会初始化类方法
类加载器
在Java中,类加载器(ClassLoader)是Java虚拟机(JVM)的一个重要组成部分,负责将.class文件加载到内存中,并转换为Class对象。
从JVM的角度,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现(HotSpot虚拟机中),是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都有Java语言实现,独立于虚拟机外部,并且全部继承自java.lang.ClassLoader。
从开发者的角度,Java中的类加载器主要分为三种:
-
启动类加载器(Bootstrap ClassLoader):
- 启动类加载器是JVM的一部分,它负责加载Java核心类库(如rt.jar、charsets.jar等),并且是所有其他类加载器的父类加载器。
- 启动类加载器是用本地代码实现的,不继承自java.lang.ClassLoader类,因此在Java中无法直接获取到启动类加载器的引用。
- 启动类加载器是最顶层的类加载器,负责加载JVM运行时需要的基础类,通常不会被Java应用程序直接使用或扩展。
-
扩展类加载器(Extension ClassLoader):
- 扩展类加载器是负责加载Java平台扩展库(如javax包下的类)的类加载器。
- 扩展类加载器是由sun.misc.Launcher$ExtClassLoader实现的,并且是启动类加载器的子类加载器。
- 扩展类加载器通常加载JRE/lib/ext目录下的jar包,或者由java.ext.dirs系统属性指定的目录中的类。
-
应用程序类加载器(Application ClassLoader):
- 应用程序类加载器也称为系统类加载器,是负责加载应用程序classpath下的类的类加载器。
- 应用程序类加载器是由sun.misc.Launcher$AppClassLoader实现的,是扩展类加载器的子类加载器。
- 应用程序类加载器加载用户自定义的类,以及第三方类库等。
我们在加入自己定义的类加载器以满足特殊的需求时,要继承java.lang.ClassLoader类,而一个自定义ClassLoader创建时如果没有指定parent,它的parent默认就是AppClassLoader
可以用这张图来表示类加载器之间的关系:
他们并非典型的继承关系,而是子指派父为自己的 parent。
当学习自定义类加载器时,以下方法是非常重要的:
1.loadClass(加载指定的Java类):
loadClass 方法是 ClassLoader 类中的一个重要方法,用于加载指定的 Java 类。
在 loadClass 方法中,首先会调用父类加载器的 loadClass 方法尝试加载类,若父类加载器无法加载,则会调用 findClass 方法进行加载。
一般情况下,开发人员不直接覆盖 loadClass 方法,而是覆盖 findClass 方法来实现自定义的类加载逻辑。
2.findClass(查找指定的Java类):
findClass 方法是自定义类加载器中一个重要的方法,用于在指定位置查找并加载指定的 Java 类。
在 findClass 方法中,开发人员可以根据自己的需求实现加载类的逻辑,比如从网络、数据库或其他特殊位置加载类文件。
3.findLoadedClass(查找JVM已经加载过的类):
findLoadedClass 方法用于查找 JVM 中是否已经加载过某个类。
通过调用 findLoadedClass 方法,可以检查某个类是否已经被加载,如果已经加载则返回对应的 Class 对象,否则返回 null。
4.defineClass(定义一个Java类):
defineClass 方法用于将字节数组转换为一个 Class 对象。
当自定义类加载器需要加载一个类时,通常会先读取该类的字节码文件,然后调用 defineClass 方法将字节数组转换为 Class 对象。
5.resolveClass(链接指定的Java类):
resolveClass 方法用于链接指定的 Java 类,即将类的二进制数据合并到 JVM 中。
在自定义类加载器加载类之后,一般需要调用 resolveClass 方法来确保类的正确链接,以便类在运行时能够正常使用。
双亲委派模型机制
一句话总结就是:一个类加载器拿到一个类后不会先去加载,而是先交由父加载器加载,由是递归,直到最顶层,当父加载器无法加载时才会委派给子加载器去加载,由是递归。
顺序是(App->Ext->Bootstrap)
展开来说:
- 当一个类加载器需要加载一个类时,首先检查该类是否已经被加载过了。如果已经被加载,直接返回已加载的Class对象。
- 如果该类没有被加载过,那么该类加载器会将这个任务委派给它的父类加载器进行加载。
- 父类加载器会按照相同的方式继续委派,直到达到最顶层的启动类加载器(Bootstrap ClassLoader)。
- 如果启动类加载器可以加载该类,则直接返回Class对象;否则,子类加载器会尝试自己加载这个类。
- 如果所有的父类加载器都无法加载该类,子类加载器会抛出ClassNotFoundException异常。
代码实现:
在java.lang.ClassLoader的loadClass()方法中,先检查是否已经被加载过,若没有加载则调用父类加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父加载失败,则抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。
①java.lang.ClassLoader的loadClass
方法:
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {synchronized (getClassLoadingLock(name)) {// 首先尝试从缓存中查找已经加载的类Class<?> c = findLoadedClass(name);if (c == null) {if (parent != null) {// 如果存在父类加载器,则委派给父类加载器加载c = parent.loadClass(name, false);} else {// 否则使用引导类加载器加载c = findBootstrapClassOrNull(name);}}if (c == null) {// 如果父类加载器无法加载,则调用自己的 findClass 方法来加载c = findClass(name);}if (resolve) {resolveClass(c);}return c;}
}
②java.lang.ClassLoader的findClass
方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {throw new ClassNotFoundException(name);
}
在 java.lang.ClassLoader
中,findClass
是一个抽象方法,具体的类加载器需要通过继承并重写这个方法来实现自己的类加载逻辑。在重写 findClass
方法时,开发人员通常会在其中实现自定义的类加载逻辑,比如从文件系统、网络或者其他地方加载类的字节码,并最终通过 defineClass
方法将字节码转换为 Class
对象。
下面以Extension ClassLoader
的 findClass
方法为例:
protected Class<?> findClass(String name) throws ClassNotFoundException {String path = name.replace('.', '/') + ".class";URL url = findResource(path); // 在扩展库路径中查找类文件对应的资源if (url != null) {try (InputStream is = url.openStream()) {byte[] classBytes = readFully(is); // 从输入流中读取类文件的字节码return defineClass(name, classBytes, 0, classBytes.length);} catch (IOException e) {throw new ClassNotFoundException("Error reading class file for " + name, e);}} else {throw new ClassNotFoundException(name);}
}
为什么要有双亲委派机制
双亲委派模型的优势在于避免了类的重复加载和破坏类的命名空间。当一个类被加载时,系统会从上至下依次检查每个类加载器,确保每个类只被加载一次。这样可以避免不同的类加载器加载同一个类导致的类冲突问题。
如何打破双亲委派机制
第一种:
1.自定义一个类加载器,继承自 java.lang.ClassLoader。
2.在自定义类加载器中重写 loadClass 方法,并在该方法中实现自定义的类加载逻辑,不再委派给父类加载器。
3.在自定义类加载器中重写 findClass 方法,用于查找和加载类的字节码。
代码示例:
public class CustomClassLoader extends ClassLoader {@Overrideprotected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {// 自定义类加载逻辑,不再委派给父类加载器Class<?> clazz = findClass(name);if (resolve) {resolveClass(clazz);}return clazz;}@Overrideprotected Class<?> findClass(String name) throws ClassNotFoundException {// 实现自定义的类查找和加载逻辑,这里简单示例直接从文件系统加载类try {byte[] classData = loadClassData(name);return defineClass(name, classData, 0, classData.length);} catch (IOException e) {throw new ClassNotFoundException(name);}}private byte[] loadClassData(String className) throws IOException {// 从文件系统加载类的字节码// 这里省略具体的加载逻辑return Files.readAllBytes(Paths.get(className));}
}
第二种:
在 Java 中,通过 Thread
类的 setContextClassLoader
方法可以实现打破双亲委派模型
1.获取当前线程的类加载器:通常情况下,Java 中的类加载操作是由当前线程的上下文类加载器执行的。
2.设置线程上下文类加载器:通过 Thread 类的 setContextClassLoader 方法,可以为当前线程设置一个特定的上下文类加载器,从而实现自定义的类加载逻辑。
3.自定义类加载器:可以自定义一个类加载器,并在其中实现特定的类加载逻辑。
4.加载类或资源:在设置了线程上下文类加载器之后,通过当前线程的上下文类加载器来加载类或资源,从而实现特定线程范围内的自定义类加载逻辑。
代码示例:
public class CustomClassLoaderExample {public static void main(String[] args) {// 创建自定义类加载器CustomClassLoader customClassLoader = new CustomClassLoader();// 创建一个新线程Thread thread = new Thread(new Runnable() {@Overridepublic void run() {// 设置当前线程的上下文类加载器为自定义类加载器Thread.currentThread().setContextClassLoader(customClassLoader);// 在当前线程中使用上下文类加载器加载类try {Class<?> clazz = Thread.currentThread().getContextClassLoader().loadClass("com.example.MyClass");// 使用加载的类进行相应的操作} catch (ClassNotFoundException e) {e.printStackTrace();}}});// 启动新线程thread.start();}
}