什么是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方法来实现