类加载器
双亲委派机制是类加载器的一种工作方式,用于控制类的加载过程。类加载器是实现双亲委派机制的具体实体。
类加载器的主要任务是将类的字节码加载到内存中,并创建对应的类对象。当Java程序需要使用某个类时,类加载器会在类路径中搜索并加载类的字节码文件。
「类加载器分为:」
- 引导类加载器:是由c++创建,在jvm创建之后加载,它负责加载JRE下lib目录的jar。
- 扩展类加载器:负责加载JER下lib的ext,ExtClassLoader,它的父加载器是引导类加载器。
- 应用程序加载器:负责加载应用程序,AppClassLoader,它的父加载器是扩展类加载器。
- 自定义加载器:自己定义的,它的父加载器是应用程序加载器。
System.out.println("String加载类:" + String.class.getClassLoader());
System.out.println("DESKeyFactory加载类:" + com.sun.crypto.provider.DESCipher.class.getClassLoader());
System.out.println("Demo加载类:" + Demo.class.getClassLoader());
输出:
String加载类:null
DESKeyFactory加载类:sun.misc.Launcher$ExtClassLoader@372f7a8d
Demo加载类:sun.misc.Launcher$AppClassLoader@18b4aac2
解释:
String的加载类是引导类加载器加载的,引导类是由c++生成的,所以看不到,是个null。
ExtClassLoader是扩展类加载器。
AppClassLoader是应用程序加载器,一般加载类默认用到AppClassLoader。
「Launcher类」
Launcher类是Java虚拟机(JVM)启动的入口类之一。它是JVM的一部分,并负责启动Java应用程序。C++引导类加载完之后会先实例化Launcher。在实例化Launcher类的时候会创建ExtClassLoader加载器和AppClassLoader加载器。
class Launcher{
private ClassLoader loader;
private static Launcher launcher = new Launcher();
public static Launcher getLauncher() {
//默认AppClassLoader
return launcher;
}
public Launcher() {
//实例化扩展类加载器,它的父类没有设置值
Launcher.ExtClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader();
//初始化AppClassLoader,并且将ext传入设置为父类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
}
... ...
}
我们也可以手动去调用加载某个类
Launcher.getLauncher().getClassLoader().loadClass("Demo.class")
双亲委派机制
双亲委派的逻辑代码实现在Launcher类中loadClass方法中。源码如下
Class<?> loadClass(String name, boolean resolve){
//从自己加载的类里边找
Class<?> c = findLoadedClass(name);
if (c == null) {
if (parent != null) {
//递归父加载器加载
c = parent.loadClass(name, false);
} else {
//ext加载器的父是null,因为它的父加载器是引导类加载器,他是c++实现的,在java中部显示
//这是最后一层,引导类加载器加载
c = findBootstrapClassOrNull(name);
}
if (c == null) {
//尝试加载,由URLClassLoader类实现
c = findClass(name);
}
}
return c;
}
尝试加载,#URLClassLoader.findClass源码
Class<?> findClass(final String name){
String path = name.replace('.', '/').concat(".class");
//判断是不是自己要加载的。jrt/ ext/
Resource res = ucp.getResource(path, false);
if (res != null) {
//真正加载class的方法
return defineClass(name, res);
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
双亲委派机制
当我们加载某个类或者手动调用loadClass时候具体流程:
Launcher.getLauncher().getClassLoader().loadClass("Demo")
因为getClassLoader默认返回AppClassLoader,所以首先用AppClassLoader去找。
- AppClassLoader去自己已经加载的类里边找,如果没有向上委托
- ExtClassLoader去自己已经加载的类里边找,如果没有向上委托。
- 引导类去自己加载的类里边找,如果没有,
- 引导类尝试加载,判断这个类是不是应该由自己加载,判断是不是JRE包路径下的,如果是加载并且返回,不是向下传播。
- ExtClassLoader判断这个类是不是ext包路径下的,如果是加载并返回,不是向下传播。
- AppClassLoader判断这个类是不是classPath路径下的,如果是加载。不是的话报错ClassNotFound。
「为什么先由AppClassLoader,而不是引导类开始?」
因为大部分类都是我们自己写的,百分之95都是存放到AppClassLoader,只有第一次加载的时候会多走一步,之后大部分都直接从AppClassLoader里边获取。
「我们新增一个String类是否可以?」
package java.lang;
public class String {
public static void main(String[] args) {
System.out.println(111);
}
}
执行报错信息:
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application
我们新增的 java.lang.String,是能够在引导类加载器的JRE包下边找到的,并且返回String类,这个返回的是jdk自带的类,并不是我们自己写的类,所以会报找不到main方法。
「jdk为什么要用双亲委派机制」?
jdk不让修改自己内部的类,沙箱安全机制,防止核心API被篡改。
避免类的重复加载,父加载器已经加载完了,自己就不用再加载了。
自定义加载器
我们要自定义加载器,只需要实现ClassLoader,重写findClass就可以。
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(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();
}
}
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader =
new MyClassLoader("C:/myClassLoader");
Class<?> clazz = myClassLoader.loadClass("com.bbk.code.User");
Object o = clazz.newInstance();
Method method = clazz.getDeclaredMethod("eat");
method.invoke(o);
System.out.println("当前MyClassLoader的类加载器:"+MyClassLoader.class.getClassLoader());
}
}
User类:
我们需要将User类编译好的class文件放到指定的目录(C:/myClassLoader),需要新建目录为com/bbk/code,执行main方法时候先要target下边的User.class删除。
public class User {
public void eat(){
System.out.println("我是User类eat方法,我的加载器是"+User.class.getClassLoader());
}
}
执行输出
我是User类eat方法,我的加载器是com.bbk.code.MyClassLoader@5a07e868
image-20230602150910148
自定义MyClassLoader继承ClassLoader类初始话时候会先初始化父类,在这时候会给自定义MyClassLoader赋值parent为AppClassLoader。具体代码体现在:
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
//默认复制为AppClassLoader
this.parent = parent;
... ...
}
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
... ...
return scl;
}
private static synchronized void initSystemClassLoader() {
... ...
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
//返回this.loader,引导类在加载Launcher类的时候会赋值为AppClassLoader
scl = l.getClassLoader();
... ...
}
如何打破双亲委派机制
意思就是不在委托父加载,直接自己加载类。双亲委派的逻辑代码实现在loadClass方法中。需要继承ClassLoader重写loadClass方法即可:。
@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);
//把去父类查找逻辑删除
//c = parent.loadClass(name, false);
if (c == null) {
c = findClass(name);
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
重新执行main方法,出现以下错误:
java.io.FileNotFoundException: C:\myClassLoader\java\lang\Object.class (系统找不到指定的路径。)
在加载User的时候,会先加载父类Object,我们打破双亲委派之后,自定义的加载器不存在Object所以会报错。
我们把Object.class放入到我们的本地文件夹中。重新执行报错:
//禁止加载java.lang 包
java.lang.SecurityException: Prohibited package name: java.lang
java.lang必须由我们引导类加载器加载,沙箱安全。我们只能让我们的引导类去加载java.lang。修改代码
@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.bbk")){
//如果不是我们自己写的类,还是走原来双亲委派逻辑
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
只有当name是由com.bbk开头的由我们加载器加载。其他的都由父加载器加载。不会报错。
tomcat如何打破双亲委派机制
问题:当有两个war包,一个war包依赖spring4.0,另一个war包依赖spring5.0,假如spring4.0的jar包加载在tomcat下的AppClassLoader中,那么spring5.0就加载不了。需要解决这两个spring共存,这两个war包互相隔离。
类加载器定义:
tomcat类加载器
引导类 -> ext类加载器 -> appext类加载器 -> 加载tomcat公共类库的类加载器 -> 有多少war包就生成多个webAppClassLoader(它就打破了双亲委派机制,只是自己加载,不会向上委托)。