聊聊ClassLoader

什么是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方法来实现
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值