JVM(四)ClassLoader类加载机制
本文将探讨类加载器是如何寻找且加载Class文件,它的应用场景以及我们如何自定义自己的类加载器。
类加载器ClassLoader
类加载器是将字节码文件加载到JVM,它具有 延迟加载 的特性,需要某个类时才会去加载。如果 同一份字节码文件 被不同类加载器加载后的Class对象,它们是不相等的:即它们不是同一个类。
回到《JVM(三)类和对象的生命周期》文章开头写的代码,打印出了每个类型的加载器,主要分为三种类型:Bootstrap classes、Extension classes和User classes。
Apple.class: sun.misc.Launcher$AppClassLoader@2a139a55
class [Lcom.deepoove.java8.def.Apple;: sun.misc.Launcher$AppClassLoader@2a139a55
Gson.class classloader:
sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@4aa298b7
String.class: null
Bootstrap Class Loader
Bootstrap启动类加载器负责加载实现了Java平台的类,包括JDK的lib目录、rt.jar等。启动类的路径是由sun.boot.class.path属性决定的,这个属性只能被引用无法被修改。
下面是我本机的启动类路径:
sun.boot.class.path=/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/resources.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/sunrsasign.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jsse.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jce.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/charsets.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/lib/jfr.jar\:
/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/jre/classes
所以String.class是由Bootstrap加载器加载,由于这个加载器不是Java编写的(C++编写),所以在上文的输出中显示为null。
所以,
java.lang.ClassLoader也是由启动类加载器加载的。
Extension Class Loader
sun.misc.Launcher$ExtClassLoader扩展类加载器负责加载JDK目录下lib/ext目录中的包,它是由Java实现的加载器,继承URLClassLoader。注意,扩展类加载器加载的必须是jar或者zip文件,不能是.class文件。
System Class Loader
sun.misc.Launcher$AppClassLoader称为应用加载器或者系统加载器,负责加载用户路径下的类,它可以通过静态方法java.lang.ClassLoader.getSystemClassLoader()获取。
双亲委派机制
我们试想一下,如果在用户路径下定义了JDK提供的某个类,如java.lang.String,那么对于整个应用来说这是糟糕的:系统加载器和启动类加载器同时都加载了一样的类,应用的行为是不确定的。
为了解决这种问题,类加载器之间有一种层次关系,每个加载器都有一个父加载器,null表示启动类加载器,可以通过加载器的getParent()方法获取父类加载器。

如上图,当我们需要加载String.class时,就会优先向上委托到Bootstrap去加载,这样保证了用户的类无法覆盖JDK的类。所有的类加载都会委托父加载器加载,当父加载器无法完成加载时,子加载器才会尝试自己去加载,如果最终子加载器无法加载该类,则会抛出找不到类的异常,这就是双亲委派机制(Parents Delegation Model)。
我们来看看java.lang.ClassLoader的loadClass源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
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();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
当我们加载类时会优先判断是否已加载findLoadedClass,然后通过双亲委派机制parent.loadClass(name, false);加载,如果加载不到,再通过findClass(name)加载,接下来会在自定义加载器类中详细描述。
需要注意的是,如果多线程尝试去加载同一个类,在类加载实现中通过互斥锁来实现并发安全。
双亲委派模型带来了两个特性:唯一性和可见性。
-
唯一性
唯一性已经说过,类被父类加载器加载,子类无法再加载。 -
可见性
被父类加载器加载的类,子类加载器是可见的,这个很容易懂,启动类加载的类在应用中是可见的。
自定义ClassLoader
Java以java.lang.ClassLoader抽象类的形式对外开放了类加载的能力,ClassLoader默认构造方法设置了父类加载器为系统加载器,loadClass方法实现了委托模型,我们只需要重写findClass方法实现自己的类加载逻辑:
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the file or network
}
实现大体上分为两步,第一步获取字节码的字节数组byte[],然后通过ClassLoader提供的defineClass方法将字节码定义成Class对象。
java.lang.ClassLoader提供了
public URL getResource(String name)和protected URL findResource(String name)方法获取资源,正如同loadClass和findClass方法。
除了继承java.lang.ClassLoader,我们还以继承或者直接使用URLClassLoader,它支持从某个目录或者jar加载类classes或者资源resources。
写到这里,我们可能会问自己:自定义类加载器的场景在哪里?下面罗列一些场景,暂时我还没有接触到:
- 字节码加密和解密处理
- class文件版本号机制,根据版本号加载不同的类
- 热部署
- 根据JNDI动态加载JDBC驱动,此方法已被SPI替代
- agent
- 隔离
反双亲委派
双亲委派是一个约定,但是某些场景下未必好使,所以出现了一些违反双亲委派的例外,最简单违反的方法是重写loadClass方法,这样就破坏了默认通过父类加载器加载的约定。
Context ClassLoader
JNDI的核心功能是由Bootstrap启动类加载器加载,但是JNDI需要加载不同厂商提供的JNDI的实现,这样就要求Bootstrap要去加载用户类路径下的类,显然这是无法做到的。
设计者为java.lang.Thread 提供了一个方法:getContextClassLoader()为每个线程返回一个Context ClassLoader,可以通过setContextClassLoader()方法进行设置上下文类加载器,在BootStrap启动类加载JNDI时的线程里就可以获取到Context ClassLoader,如果没有设置,它就是AppClassLoader(实际上是设置了当前线程上下文类加载器为系统类加载器),然后通过它来加载用户路径下的类。
WebApp ClassLoader
web应用容器的类加载模型和双亲委派机制的原则大体上相同,但是也有不一样的地方,jetty和tomcat都有个WebAppClassLoader,为了做到应用之间隔离它们没有遵从双亲委派,这里我们以tomcat为例:

上面这张图摘自tomcat官方文档,可以看出来它也是一个父子树结构,遵循唯一性和可见性,但是webapp类加载器不遵循双亲委派机制,它没有优先委托父加载器加载,而是 优先从自己的web应用类路径加载,这样做的原因是每个webapp都希望相互之间隔离,每个应用只对自己 /WEB-INF/classes和/WEB-INF/lib 目录下类可见,避免相互之间影响而不对其它应用可见。注意的是,WebappClassLoader会最先委托BootStrap加载JRE提供的类,因为这些类不该被重新定义。,下面是它的加载器加载顺序:
- Bootstrap classes of your JVM
- /WEB-INF/classes of your web application
- /WEB-INF/lib/*.jar of your web application
- System class loader classes
- Common class loader classes
具体源码在org.apache.catalina.loader.WebappClassLoaderBase.loadClass(String, boolean)方法中,这里不再赘述。
参考资料
总结
ClassLoader提供了很多方法,ClassLoader默认执行了方法ClassLoader.registerAsParallelCapable,子类如果需要支持并发加载也需要调用这个方法,类加载器不仅仅可以加载类,还可以获取资源Resource,我们可以充分利用类加载器的特性去使用它,比如指定一个包名,然后加载所有的类并且扫描类的注解,我们也可以实现自己的类加载器,从而实现更多更有用的功能。