java里有如下几种类加载器:
引导类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,如:rt.jar、charsets.jar等等
扩展类加载器:负责加载支持JVM运行的位于jre目录下的ext扩展目录中的jar包
应用程序类加载器:负责加载classpath路径下的类包,主要就是加载开发人员编写的程序代码类
自定义加载器:负责加载自定义类路径下的类文件
类加载机制的委派层级结构如下:
其实这个加载过程就包含一个双亲委派机制。这个委派机制的大致流程是:
先判断当前加载器是否已加载目标类,如已加载直接返回,如未加载则委托父加载器加载;
父类加载器拿到目标类后先判断自己是否已加载目标类,如已加载则返回该类,如未加载再委托自己的父加载器加载。如此递归循环,直到当前类加载器没有父加载器(即当前加载器为引导类加载器),这时交由引导类加载器去加载;
引导类加载器加载目标类时现在自己的类路径下找寻目标类文件,如找到了则直接加载该类,如未找到则向下委托子类加载器去加载。直到找到该类文件且成功加载为止
我们以加载User对象为例,流程图如下:
双亲委派机制说简单点就是:逐级向上委托父加载器加载直到引导类加载器,然后再逐级向下委托子加载器加载
接下来我们结合上面的流程图再来看下相关的核心源码:
protected synchronized Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
//判断当前类加载器是否已加载目标类
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//当前类加载器有父类加载器,将目标类委托给父类加载器加载(注意:这里其实是递归)
c = parent.loadClass(name, false);
} else {
//无父类加载器则委托给引导类加载器加载,(只有引导类没有父类加载器)
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
//调用URLClassLoader的findClass方法在当前类加载器的类路径中寻找目标类文件,
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
沙箱安全机制:防止核心API库被随意篡改
保证被加载类的唯一性:在这种委派机制下,同一应用中的class文件永远只会加载一次。
类加载器两大核心方法:loadClass(String name) 和 findClass(String name),其中双亲委派机制就是通过递归调用loadClass方法来实现的。我们只需有针对性的重写该方法即可打破该委派模型,首先我们自定义一个类加载器:CustClassLoader.class
package com.htc.jvm;
import java.io.FileInputStream;
/**
* @author: htc
* @date: 2020/10/22.
* @descr: 自定义类加载器-打破双亲委派机制
*/
public class CustClassLoader extends ClassLoader {
private String classPath;
public CustClassLoader(String classPath) {
this.classPath = classPath;
}
private byte[] loadByte(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
try {
byte[] data = loadByte(name);
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
/**
* 重写类加载方法,实现自己的类加载逻辑,打破双亲委派
*
* @param name
* @param resolve
* @return
*/
@Override
protected Class> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class> c = findLoadedClass(name);
if (c == null) {
/**
* 指定自定义类的加载方式,其他类走双亲委派机制
*/
if (!name.startsWith("com.htc.jvm")) {
c = this.getParent().loadClass(name);
} else {
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
}
再创建一个测试类:
public class TestCustClassCloader {
public static void main(String[] args) throws Exception {
String classPath ="D:\\tmpPath";
CustClassLoader classLoader = new CustClassLoader(classPath);
Class clazz = classLoader.loadClass("com.htc.jvm.Test");
//通过反射实例化该对象
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("sou", null);
method.invoke(obj, null);
System.out.println(clazz.getClassLoader().getClass().getName());
}
}
在执行测试了之前我们需在D:\tmpPath\com\htc\jvm目录下创建Test.java文件,并通过javac命令对其进行编译,编译后的Test.class文件也存放在该路径下。然后再执行测试类。控制台输出如下:
Connected to the target VM, address: '127.0.0.1:54493', transport: 'socket'
this is sou method !
com.htc.jvm.CustClassLoader
Disconnected from the target VM, address: '127.0.0.1:54493', transport: 'socket'
Process finished with exit code 0
且通过断点调试我们可以发现,在加载Test类的时候,CustClassLoader类加载器并没有委托给父类加载,而是自身直接加载Test类!