双亲委派机制分析

什么是双亲委派?
双亲委派机制有4种类加载器为:
- 自定义(UserClassLoader)->应用/系统(App/SystemClassLoader)->扩展类(ExtClassLoader)->启动(BootstrapClassLoader)类加载器。
加载过程简述:
- 当一个类加载某个类.class(需要编译即javac Xx.java>>Xx.class)的时候,不会直接去加载,而是自定义会委托应用/系统,应用/系统会委托扩展,扩展会委托启动类加载器尝试去加载,如果启动类加载器不加载这个,就交给扩展,扩展不行就应用/系统,一层层的下去,然后最终加载到这个.class类。
加载的量:
- 不是一次性加载,而是按需动态加载,不然一次性加载内存可能会爆。
 
双亲委派优点?
1 安全,可避免用户自己编写的类动态替换Java的核心类,如java.lang.String
2 避免全限定命名的类重复加载(使用了findLoadClass()判断当前类是否已加载)
 

双亲委派的面试题
题目:可不可以自己写个String类(也是自定义的String为何没加载到?) - 阿里
不可以。因为在类加载中,会根据双亲委派机制去寻找当前java.lang.String是否已被加载。由于启动类加载器已在启动时候加载了所以不会再次加载,因此使用的String是已在java核心类库加载过的String,而不是新定义的String。
代码:
//这里为了测试,将其的包名改成与jdk的rt.jar中的java.lang.String一致。
package java.lang;   
public class String {
    static {
        System.out.println(11);
    }
    private String(int i) {
        System.out.println(i);
    }
    //注意:核心类库的String是没有main方法的,因为他找到的核心类库String, 所以报找不到main()方法错误.
    public static void main(java.lang.String[] args) {
        String s = new String(1);
        System.out.println(s);
    }
}
 
 
双亲委派的加载过程?
看完上面的, 我好奇它的每个类加载器有什么区别?代码里是怎么加载的?流程是怎么加载的?
1 每个类加载器有什么区别?
BootstrapClassLoader: 
- 最顶层类加载器,加载java核心类库即%JRE_HOME%\lib下的rt.jarcharsets.jar和class等, 可通过java -Xbootclasspath/a:path(追加)、-Xbootclasspath/p:path(优先加载)、-Xbootclasspath:bootclasspath(替换jdk的rt.jar的加载)指定。
 
ExtClassLoader:
- 扩展类加载器,加载%JRE_HOME%\lib\ext的jar和class文件,可用-Djava.ext.dirs=./plugin:$JAVA_HOME/jre/lib/ext (":"是作为分隔符,代表./plugin和ext目录的都被扩展类加载器加载)指定。
 
App/SystemClassLoader:
- 应用/系统类加载器,加载当前classpath的所有类。
 
XxxClassLoader:
- 用户自定义的类加载器,默认使用双亲委派,委托上级来加载。
 
代码里是怎么加载的?
首先找到AppClassLoader、ExtClassLoader,这两个类均在Launcher.java内。从代码分析得如下:
---------------------------------------------------------------------------------------
...................
private static String bootClassPath = System.getProperty("sun.boot.class.path");    //35行
...................
String var0 = System.getProperty("java.ext.dirs");    //297行
...................
final String var1 = System.getProperty("java.class.path");  //164行
...................
---------------------------------------------------------------------------------------
然后测试以上三个,代码及输出如下:
System.out.println(System.getProperty("sun.boot.class.path")
.replaceAll("C:\\\\software\\\\programme\\\\Java\\\\jdk1.8.0\\\\jre", ""));//启动类加载路径
System.out.println(System.getProperty("java.ext.dirs"));//扩展类加载路径
System.out.println(System.getProperty("java.class.path"));//应用/系统类加载路径
 
注:结合1中每个类加载器的区别,就可以知道他们的加载目录究竟在哪里。
 
知道了他们的加载路径,接下来我们探讨下加载的顺序
我们先看如下代码:
ClassLoader classLoader = Demo06.class.getClassLoader();
使用debug模式调试可见当前类加载器先找到的parent属性(上级类加载器)为Launcher下AppClassLoader,然后AppClassLoader的parent属性(上级类加载器)为ExtClassLoader, ExtClassLoader的parent为null
 
那么他们的这个parent属性是如何赋值的?详情分析如下代码:
//首先AppClassLoader、ExtClassLoader都在Launcher下,其结构如下:
public class Launcher {
    ..................................
    static class AppClassLoader extends URLClassLoader {.......}
    ..................................
    static class ExtClassLoader extends URLClassLoader {.......}
    ..................................
}
此上面的类我们可以分析双亲委派是如何设置上级类加载器的,过程如下:
public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        //第一步 获取Ext类加载器,Ext类加载器构造方法中初始化了其上级(这里下面代码讲解)
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }
    try {
        //第二步 获取App类加载器,App类加载器会将var1(即Ext类加载器)传入,然后最终也是传入到其构造方法进行初始化其上级(这里下面代码讲解)
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }
    //第三步 将获取到的App类加载器设置为当前线程类加载器
    Thread.currentThread().setContextClassLoader(this.loader);
    ..........................
}
看完上面的代码,我们来细讲一下第一步、第二步究竟是怎样初始化parent,可先见其继承体系图如下:
 
注:从继承体系可见AppClassLoader、ExtClassLoader都继承了URLClassLoader、ClassLoader。而ClassLoader里面正定义了这个this.parent
public abstract class ClassLoader {
    ...................
    private final ClassLoader parent //上级的类加载器
    ...................
    protected ClassLoader() {  //第一种初始化方法(无参),直接传入系统类加载器
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    ...................
    //第二种有参,传入对应的parent,比如AppClassLoader传入的parent是ExtClassLoader实例,ExtClassLoader传入的是null。
    protected ClassLoader(ClassLoader parent) {  
        this(checkCreateClassLoader(), parent);
    }
    ...................
    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent //然后无论是哪种,最后都会传入到这个构造方法,然后赋值给this.parent
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }
}
"如何设置上级类加载器"分析总结:在launcher中app、ext类加载器已经初始化对应的构造方法,然后其对应的构造方法都会调用super(parent)然后分别将ext、null传入最终传到ClassLoader的构造方法中的this.parent = parent。其传递给过程取ExtClassLoader作为示例如下:
//1 Launcher下的ExtClassLoader
public ExtClassLoader(File[] var1) throws IOException {
    super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
    SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}
//2 URLClassLoader下
public URLClassLoader(URL[] urls, ClassLoader parent,
    URLStreamHandlerFactory factory) {
    super(parent);
  ...............
}
//3 SecureClassLoader下
protected SecureClassLoader(ClassLoader parent) {
    super(parent);
    ........................
}
//4 ClassLoader下
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent //然后无论是哪种,最后都会传入到这个构造方法,然后赋值给this.parent,然后设置好了上级类加载器
}
 
 
上面我们分析完了每个上级类加载器是怎么拿到的,接下来我们探讨下类加载器间是如何进行双亲委托的?
 
由图分析得:双亲委托机制采用的是"向上委托,向下查找",其步骤如下:
第一步(向上委托) 当前类加载对.class进行加载,先会找到上级类加载器AppClassLoader,然后去缓存查是否有已加载的类,如果没有则去上级ExtClassLoader缓存查找是否有已加载的类,如果没有则再往上Bootstrap缓存找是否有已加载的类,如果没有就会进入第二步,反之上面任何一步缓存查找有的话,都会直接返回缓存里加载了的.class,而不会继续往上级找
 
第二步(向下查找) 第一步缓存找不到时就会进入第二步。此时已经到了Bootstrap,Bootstrap会先到其对应的加载目录(sun.mic.boot.class路径)去看看当前有没这个类加载,如果有就加载返回返回;没有则往下级ExtClassLoader的对应加载目录(java.ext.dirs路径)找,有就加载返回,无就往下走;走到AppClassLoader然后去其对应加载目录(java.class.path路径)加载,有就加载,没有则让子类找,如果还失败就抛异常,然后调用当前ClassLoader.findClass()方法加载
:findClass是子类实现的,所以是用来自定义类加载器的。
 
其中这个加载过程涉及到了几个重要方法:loadClass、findLoadClass、findClass、defindClass。
 
//1 loadClass分析步骤(ClassLoader.java中):
//- finadLoadClass检查当前class是否被加载过;
//- 执行父加载器loadClass,如果没加载到会一直loadClass到Bootstrap ClassLoader,此时parent=null;
//-   如果向上委托没加载成功就使用findClass向下查找;
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(name)) { //loadClass加了synchronized,是同步的
        // 检查当前全限定类名(截图如上)下的包是否已被加载过,最终调用本地方法中(native final Class<?> findLoadedClass0(String name))
        Class<?> c = findLoadedClass(name);
        //如果当前没被加载过,就重新加载一次
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                //!!重点!!看当前有没上级类加载器,一般是AppClassLoader、ExtClassLoader,
                //而且这个loadClass会调用上级的loadClass,一直调用到parent=null的loadClass()
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {//!!重点!!如果当前parent为null,那应该就是Bootstrap classLoader(因为这个是底层是C++,所以不直接调)
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {}
            
            if (c == null) {
                long t1 = System.nanoTime();
                //上面两个重点的父类加载器没找到,则调用findClass
                c = findClass(name) //这个方法直接抛出异常(采用模板方法模式),是给子类实现
 
                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {//为true调用resolveClass
            resolveClass(c);
        }
        return c;
    }
}
 
上面解析了系统自带的几个类加载器如Ext、bootstrap是怎么加载的,
但是为什么还需要自定义类加载器??
1 从非标准来源加载代码:由于系统提供的类加载器均加载的是指定目录,所以当我们需要加载非系统指定目录如C:/xx/xxxx.class时需要自定义类加载器、数据库、云端等
2 加密:将编译后的代码加密,然后用自定义类加载器去先解密,然后再加载。
自定义类加载器过程如下(自定义默认parent为AppClassLoader):
1 继承ClassLoader抽象类
2 重写findClass() 
3 重写的findClass()中调用defineClass()
其代码实现过程如下:
  • 第一步:编写Test测试类Test.java, 然后使用javac Test.java编译成Test.class并拷贝到C:\test目录下
package com.lisam.test;  //放在其他非规定位置时,加这个会报找不到
public class Test {
    public void test(){
        System.out.println("test成功");
    }
}
  • 第二步:编写自定义类加载器
// 1 继承ClassLoader抽象类
class MyClassLoader extends ClassLoader {
    private String path;
    MyClassLoader(String path) {
        this.path = path;
    }
 
    // 2 重写findClass()
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = new File(this.path);
        try {
            byte[] bytes = getClassBytes(file);
            //3 defineClass方法可以把二进制流字节组成的文件转换为一个java.lang.Class
            return this.defineClass(name, bytes, 0, bytes.length);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return super.findClass(name);
    }
    //这里反正当前文件的比特流即可
    private byte[] getClassBytes(File file) throws IOException {
        //使用字节流获取.class
        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);
        while (true) {
            int     i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
         }
        fis.close();
        return baos.toByteArray();
    }
}
  • 第三步 使用并输出结果
@Test
public void test01() throws Exception {
    MyClassLoader mcl = new MyClassLoader("C:\\test\\Test.class");
    Class<?> clazz = Class.forName("Test", true, mcl);
    Object obj = clazz.newInstance();
    Method method = clazz.getDeclaredMethod("test", null);
    method.invoke(obj, null);
    System.out.println(obj);
    System.out.println(obj.getClass().getClassLoader());//打印出我们的自定义类加载器
}
 
  • 17
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值