本文只涉及java 的classloader体系以及classloader对class的加载行为,不涉及一个class的加载过程和生命周期
ClassLoader 层级
jvm classloader 分为3层,顶层是拿C语言写的Bootstrap classloader,我们称之为JVM类加载器,主要加载的是classpath 下rt.jar,rt.jar 中主要是java的一些基础类,例如Integer, String 等
第二层是ext class loader 已经拿java语言去实现了,主要是加载JDK下ext包下的类,ext包下有类似DNS相关的类,zip解析相关的类,加密解密的类等其他一些java 扩展类,是现在Lanucher 的 ExtClassLoader
第三层是 系统类加载器,也可以称为应用类加载器,主要加载应用中的类,所有的自定义类都是继承自这个类,实现是在Launcher中的AppClassLoader
classLoader 双亲委托机制
首先来解释双亲委托机制,这个机制主要是定义了一个类在加载中所遵循的规则,例如,A.class 类的加载首先是由应用类加载器,但是类并不是由应用类加载器进行加载,而是会转到上一层的类加载器(ExtClassLoader),ExtClassLoader也不会直接加载,同样的他会交由Bootstrap类加载器进行加载,只有当这个加载器加载不了的时候,才会交由下层类加载器加载,最终A.class会交由应用本身的类加载器进行加载。这种机制的好处就在于,Java本身的类不会被别的加载器加载,只能由Bootstrap中加载,这样能保证被加载类的安全性,因为只有这样,加载出来的Class才不会被篡改,例如,如果Integer.class可以在应用类加载器中加载,那么应用类加载器可以在自己本身的加载路径中模拟一个全路径名一模一样的class,而这个class可以是一个会引起JVM OOM的类,那JVM的安全将不能被保证了,因为有了每一层加载器加载制定范围内限定权限下的class,不至于让开发者能人为破坏JVM
class在classloader中加载的顺序(ClassLoader中类加载源码解析)
现在我们来看一下源码中是怎么实现对class的加载,AppClassLoader和ExtClassLoader都是继承自URLClassLoader,所以我们直接看下里面的实现,主要的是三个方法,loadClass,defineClass,findClass
loadClass
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//先查看当前class是否已经被加载过
Class c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//如果没有被加载过,则交给父加载器进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果没有附加载器,直接交给Bootstrap classLoader 加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 此处类未加载到不进行excepiton处理,避免类加载中断
}
if (c == null) {
// 通过双亲委托,交由父加载器没有加载到对应的类时,// 当前类自己进行class load
long t1 = System.nanoTime();
// 在指定的类路径下查找类
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();
}
}
// 类加载时可以指定知否需要初始化class,不进行初始化的话,在class被用到时再resolve
if (resolve) {
resolveClass(c);
}
return c;
}
}
findClass
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
try {
// 通过传入的path路径 (name即path类的全路径)加载类
return AccessController.doPrivileged(
new PrivilegedExceptionAction<Class>() {
public Class run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
// 通过全路径名加载class,define方法将二进制流转成class
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
throw new ClassNotFoundException(name);
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
}
defindClass方法在这边就不介绍源码了,源码可读意义不大,主要的功能是将指定了资源路径名的class解析成为一个JVM 的class对象,用到了很多sun.misc包下的API
上下文类加载器
java 默认的类加载机制使用的是双亲委托的方式,但是有这么一种场景,Java一般定义了供第三方SPI去实现的统一接口,一般这些接口在rt.jar中,而第三方代码库的实现代码一般存在在自己集成的jar中,这个时候,如果依据我们之前认知的类加载方式--双亲委托机制,rt.jar中的接口类肯定会交由Boostrap进行加载,这个时候,在加载到具体实现的时候,默认是通过当前类加载器进行具体类的加载,Boostrap是找不到第三方类库的实现类的,这样类加载就失败了;所以在这种场景下,上下文类加载器应运而生,在加载第三方类库前,调用类先通过Thread的getContextClassLoader获取上下文加载器,一般默认的加载器是AppClassLoader,然后通过该加载器进行第三方类库代码的加载,应用类加载器本身就有加载应用classpath下面的依赖包,所以就会加载第三方类库的实现类,以JAXP的DocumentaryBuilderFactory为例
// 实际获取SPI实现类的方法
private static Class getProviderClass(String className, ClassLoader cl,
boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
{
try {
if (cl == null) {
if (useBSClsLoader) {
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
} else {
// 获取上下文类加载器
cl = ss.getContextClassLoader();
if (cl == null) {
throw new ClassNotFoundException();
}
else {
// 实际loadClass
return cl.loadClass(className);
}
}
}
else {
return cl.loadClass(className);
}
}
catch (ClassNotFoundException e1) {
if (doFallback) {
// Use current class loader - should always be bootstrap CL
return Class.forName(className, true, FactoryFinder.class.getClassLoader());
}
else {
throw e1;
}
}
}