聊聊ClassLoader

来源:互联网 发布:动态矩阵控制 编辑:程序博客网 时间:2024/06/02 04:16

什么是ClassLoader

java程序在编写的时候都是.java文件,但真正去运行的时候都是加载编译后的.class文件,而不是.java文件;一般项目都不会由单个类构成,这涉及到类的依赖,相互协作完成复杂的业务功能,而在程序启动的时候不会一次性加载程序所要用到的所有class文件,而是根据需要,在用到的时候通过类加载器加载到内存中,然后被其他class引用;

ClassLoader是怎样工作的

java默认提供了3个ClassLoader

java提供了3个ClassLoader,用来加载不同的ClassLoader

  • bootstrap ClassLoader:该加载器是用C++编写实现的,由jvm在启动的时候初始化,主要加载java_home/lib和JAVA_HOME/jre/classes下的核心类库的部分类,通过设置VM options:-verbose:class,可以看到一些rt.jar包下的核心类被加载,输出类似下面,需要注意的是即使将自定义的类打成jar包,该ClassLoader也不认,bootstrap ClassLoader有安全机制能够识别陌生的非原生的类:

    [Opened /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar][Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home/jre/lib/rt.jar]...
  • ExtClassLoader:负责加载java_home/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库
  • ApClassLoader:负责加载用户类路径下(java -classpath或-Djava.class.path变量所指的目录)的类

ClassLoader的双亲委派机制

  • 什么是双亲委派机制呢?打个不是很恰当的比喻,一个人突然去世了,没有将遗产进行分割,需要遗产继承,这个人的孙子怎么样才能拿到这份遗产呢,首先要看看这个人的配偶还在不在,如果在,就优先给配偶了,孙子什么也得不到,如果配偶不在,那么需要看看这个人的儿子女儿是否还在,就这样一层一层的条件筛选下来;ClassLoader的双亲委派机制也是这样,比如在这个时候需要加载A类(前提条件,之前没有被加载过),查看当前的ClassLoader有没有parent,如果有,那么交给parent ClassLoader加载,直到parent为null(ClassLoader的parent为null的时候,表示其parent为bootstrap ClassLoader),这个时候调用bootstrap ClassLoader加载;如果parent ClassLoader不能加载指定的类,那么交由自己加载,如果都不能加载,就要报ClassNotFoundException异常了;
  • 那么如果A类已经被加载过了呢?其实每个ClassLoader都有自己的缓存,每次loadClass的时候都会先检查当前ClassLoader的缓存,如果查找不到,再通过双亲委派机制进行loadClass;这个过程也有点类似遗产继承,如果遗产已经被分割,那么某份被划分好的遗产的继承就相当于是ClassLoader的缓存中class,不用再去找parent来loadClass了
  • ClassLoader的双亲委派机制是通过ClassLoader包含一个父ClassLoader(parent)成员来实现的,看下代码,除了bootstrap ClassLoader是用C++编写的,其他的几个ClassLoader(ExtClassLoader,APPClassLoader)都继承ClassLoader,ClassLoader的构造方法有设置parent的逻辑,下边是截取的小段ClassLoader的代码,

    public abstract class ClassLoader {    private static native void registerNatives();    static {        registerNatives();    }    // The parent class loader for delegation    // Note: VM hardcoded the offset of this field, thus all new fields    // must be added *after* it.    private final ClassLoader parent; }

ClassLoader的另一个概念——命名空间

  • 命名空间其实跟其他的一些命名空间的概念类似,比如java的类,如何确定两个类是同一个类呢,是不是需要包名+类名呢,那ClassLoader的命名空间也类似,需要类的全限定名(即包名+类名)和加载此类的ClassLoader来共同确定,很好理解,同一个类A,由ClassLoader1和ClassLoader2加载是不一样的;而类的访问限定也是通过类加载器来限定的,比如自己定义了一个java.lang.A类,A这个时候是不能访问java.lang包下的某个类的protected成员的,验证这个规则的代码及运行结果如下:
package java.lang;public class TestProtected {    public static void main(String[] args) {        System.out.println("haha");        Thread thread = new Thread();        try {            thread.clone();        } catch (CloneNotSupportedException e) {            e.printStackTrace();        }    }}

运行结果

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)    at java.lang.ClassLoader.defineClass(ClassLoader.java:761)    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)    at java.net.URLClassLoader$1.run(URLClassLoader.java:368)    at java.net.URLClassLoader$1.run(URLClassLoader.java:362)    at java.security.AccessController.doPrivileged(Native Method)    at java.net.URLClassLoader.findClass(URLClassLoader.java:361)    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)    at java.lang.Class.forName0(Native Method)    at java.lang.Class.forName(Class.java:264)    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:123)

看看上面的结果,编译是没有任何问题的,因为这个命名空间是在运行时起作用的,而编译的时候,TestProtected确实跟Thread位于同一个包下,但是在运行的时候抛异常了;

  • 双亲委托机制可以避免类加载混乱,比如两个ClassLoader都需要加载java.lang.String,如果不采用双亲委托机制,这样java.lang.String会被两个ClassLoader加载2遍,而由于命名空间的限制,这两个ClassLoader加载的java.lang.String虽然是同一份字节码,但不是相同的,在程序赋值的时候会出现ClassCastException(这个异常经常在类型强制转换的时候出现),那么就很混乱;而采用双亲委托机制,java.lang.String会由bootstrap ClassLoader加载,所有只会加载一次,不会出现ClassCastException的情况
  • 有些场景需要打破双亲委托机制,比如tomcat的类加载机制(对于tomcat的类加载机制,后面再仔细研究)

看看源码

ClassLoader最重要的几个方法是:
public Class<?> loadClass(String name) throws ClassNotFoundException
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
protected final Class

protected ClassLoader(ClassLoader parent) {    //指定parent ClassLoader,如果parent为null,那么就相当于指定bootstrap ClassLoader为parent    this(checkCreateClassLoader(), parent);}protected ClassLoader() {    //不带参数的构造函数默认采用AppClassLoader:getSystemClassLoader()    this(checkCreateClassLoader(), getSystemClassLoader());}private ClassLoader(Void unused, ClassLoader parent) {    this.parent = 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;    }}
  • 看看加载类的入口:loadClass方法(已删减一些关系不大的代码);注意jdk8跟jdk6loadClass方法已经被重写了,有稍微一点不同,但大体上是一致的
public Class<?> loadClass(String name) throws ClassNotFoundException {    return loadClass(name, false);}protected Class<?> loadClass(String name, boolean resolve)    throws ClassNotFoundException{    synchronized (getClassLoadingLock(name)) {        // 首选,查找以及被加载的class        Class<?> c = findLoadedClass(name);        if (c == null) {            //如果没有在缓存中找到需要加载的class,通过双亲委托机制加载            try {                if (parent != null) {                //如果有parent,那么通过parent加载                    c = parent.loadClass(name, false);                } else {                //如果没有parent,那么通过bootstrap ClassLoader加载                    c = findBootstrapClassOrNull(name);                }            } catch (ClassNotFoundException e) {            }            //如果双亲委托机制也加载不到,则通过当前ClassLoader加载,加载不到就抛出异常            if (c == null) {                c = findClass(name);            }        }        //很多时候resolve都是false,但会在以后实际需要用的时候进行resolve,即延迟resolve        if (resolve) {            //解析装载类:验证Class以确保类装载器格式和行为正确;准备后续步骤所需的数据结构;解析所引用的其他类。            resolveClass(c);        }        return c;    }}
  • 在jdk8的源码中,有一行获取锁的过程:getClassLoadingLock(name),而且这个过程被加锁了,这个代码在jdk6中是没有的,这个应该是jdk8改进的地方吧,来看看getClassLoadingLock方法
protected Object getClassLoadingLock(String className) {    Object lock = this;    //parallelLockMap在构造函数中进行初始化:ConcurrentHashMap    if (parallelLockMap != null) {        Object newLock = new Object();        lock = parallelLockMap.putIfAbsent(className, newLock);        if (lock == null) {            lock = newLock;        }    }    return lock;}

从前面ClassLoader的构造方法中看出,如果当前的加载器具备并行能力,就new一个ConcurrentHashMap;从上面的getClassLoadingLock看出,如果parallelLockMap为null,说明不具备并行能力,那么直接返回this,也就是对this进行同步操作;如果具备并行能力,那么需要进行锁操作,如果name对应的key在parallelLockMap有lock存在,那么返回存在的lock,否则返回重新new的lock,这样就实现了并发的同步控制

自定义ClassLoader

  • 看到defineClass方法是final,所已不能重写defineClass方法,直接用ClassLoader提供的就可以了,而loadClass和findClass都是非final的,可以自定义,如果有需要不采用双亲委托机制,那么在重写loadClass的时候将双亲委托机制改写掉就好了,通常情况下自定义ClassLoader主要通过重写findClass方法来实现
原创粉丝点击