什么是双亲委派?



双亲委派机制有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: 



、charsets.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



 



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"));//应用/系统类加载路径



双亲委派机制分析_类加载机制_02

 



注:结合1中每个类加载器的区别,就可以知道他们的加载目录究竟在哪里。



 



知道了他们的加载路径,接下来我们探讨下加载的顺序



我们先看如下代码:



ClassLoader classLoader = Demo06.class.getClassLoader();



当前类加载器先找到的parent属性(上级类加载器)为Launcher下AppClassLoader,然后AppClassLoader的parent属性(上级类加载器)为ExtClassLoader, ExtClassLoader的parent为null。



双亲委派机制分析_双亲委派_03

 



那么他们的这个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,可先见其继承体系图如下:



 



双亲委派机制分析_Java_04

注:从继承体系可见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,然后设置好了上级类加载器
 
   
}



 



 



上面我们分析完了每个上级类加载器是怎么拿到的,接下来我们探讨下类加载器间是如何进行双亲委托的?



双亲委派机制分析_双亲委派_05

 



由图分析得:双亲委托机制采用的是"向上委托,向下查找",其步骤如下:



缓存查是否有已加载的类,如果没有则去上级ExtClassLoader缓存查找是否有已加载的类,如果没有则再往上Bootstrap缓存找是否有已加载的类,如果没有就会进入第二步,反之上面任何一步缓存查找有的话,都会直接返回缓存里加载了的.class,而不会继续往上级找。



 



对应的加载目录(sun.mic.boot.class路径)去看看当前有没这个类加载,如果有就加载返回返回;没有则往下级ExtClassLoader的对应加载目录(java.ext.dirs路径)找,有就加载返回,无就往下走;走到AppClassLoader然后去其对应加载目录(java.class.path路径)加载,有就加载,没有则让子类找,如果还失败就抛异常,然后调用当前ClassLoader.findClass()方法加载。



注:findClass是子类实现的,所以是用来自定义类加载器的。



 



其中这个加载过程涉及到了几个重要方法:loadClass、findLoadClass、findClass、defindClass。



双亲委派机制分析_Java_06

 



//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;
 
  
    }
 
  
}
//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());//打印出我们的自定义类加载器
 
  
}



双亲委派机制分析_双亲委派_07

 

 




参考:







重点文章:



Java探针:



Java类加载机制:



双亲委派模式及优势:



如何自定义类加载器: