天天看点

面试官:说一下双亲委派机制和如何打破双亲委派机制?

作者:Java面试技术栈

类加载器

双亲委派机制是类加载器的一种工作方式,用于控制类的加载过程。类加载器是实现双亲委派机制的具体实体。

类加载器的主要任务是将类的字节码加载到内存中,并创建对应的类对象。当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去找。

  1. AppClassLoader去自己已经加载的类里边找,如果没有向上委托
  2. ExtClassLoader去自己已经加载的类里边找,如果没有向上委托。
  3. 引导类去自己加载的类里边找,如果没有,
  4. 引导类尝试加载,判断这个类是不是应该由自己加载,判断是不是JRE包路径下的,如果是加载并且返回,不是向下传播。
  5. ExtClassLoader判断这个类是不是ext包路径下的,如果是加载并返回,不是向下传播。
  6. 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(它就打破了双亲委派机制,只是自己加载,不会向上委托)。

上一篇: #77_edit

继续阅读