原文:Class Loaders in Java by baeldung
翻译:陈同学
可以参考笔者另一篇译文 深入JVM内幕 中的类装载器部分
类加载器简介
Class loaders属于JRE的一部分,负责在运行时将Java类动态加载到JVM。得益于class loaders,JVM在无需知晓底层文件或文件系统时就可以运行Java程序。
此外,Java类是按需加载,并不会一次全部加载到内存中。Class loaders负责将类加载到内存。
在本教程中,我们将聊聊几种不同的内置class loaders,它们如何工作以及如何创建自定义的class loader。
几种内置类加载器
我们先以一个简单例子了解下不同类被类加载器加载的区别(PrintClassLoader为当前测试类)。
public void printClassLoaders() throws ClassNotFoundException {
System.out.println("Classloader of this class:"
+ PrintClassLoader.class.getClassLoader());
System.out.println("Classloader of Logging:"
+ Logging.class.getClassLoader());
System.out.println("Classloader of ArrayList:"
+ ArrayList.class.getClassLoader());
}
结果如下:
Class loader of this class:sun.misc.Launcher$AppClassLoader@18b4aac2
Class loader of Logging:sun.misc.Launcher$ExtClassLoader@3caeaf62
Class loader of ArrayList:null
可以看到,有三种不同class loader:application(系统类加载器)、extension(拓展类加载器)和bootstrap(启动类或引导类加载器,显示为null)。
application class loader加载上面样例代码所属的类,application class loader 或(system class loader)用于加载classpath下的文件,是用于加载应用程序class的加载器。
然后,extension class loader加载了上面的 Logging 类,它用于加载Java核心库之外的拓展类。
最后,bootstrap class loader加载了 AarrayList 类,bootstrap(或 primordial) class loader是其他所有class loader的父类。
最后一行 ArrayList 之所以输出值为 null,这是因为bootstrap class loader是由native代码所写,所以它不会以Java类的形式体现。由于这个原因,bootstrap class loader在不同JVM之中行为会有所不同。
让我们了解下几种不同class loader。
启动类加载器(Bootstrap Class Loader)
Java类由 java.lang.ClassLoader 的实例进行加载,不过,class loader本身也是Java类,那么 java.lang.ClassLoader 又是由谁加载的呢?
这就是Bootstrap class loader大显身手的地方。它主要负责加载JDK核心类,通常是 rt.jar 和位于 $JAVA_HOME/jre/lib 下的核心库。此外,它也是所有其他 ClassLoader实例的父类。
Bootstrap class loader 是JVM核心之一,由Native代码所写,这点在上述例子中提到过。不同平台Bootstrap class loader可能有不同的实现。
拓展类加载器(Extension Class Loader)
Extension class loader是Bootstrap class loader的子类,负责加载Java核心库外的拓展类,正因如此所有的应用程序都能够运行在Java平台上。
Extension class loader从JDK拓展目录加载类,通常是 $JAVA_HOME/lib/ext 目录或 java.ext.dirs 系统属性中配置的目录。
系统类加载器(System Class Loader)
System class loader是Extensions class loader的子类,负责加载所有应用程序级别的类到JVM,它会加载classpath环境变量或 -classpath以及-cp命令行参数中指定的文件。
Class Loaders是如何工作的?
Class loaders是JRE的一部分。当JVM请求一个类时,class loaders会通过类的全限定名尝试加载类并将class definition加载到runtime。
java.lang.ClassLoader.loadClass()方法负责通过类的全限定名将class definition加载到runtime。
如果class尚未加载,class loader会将加载请求委派给父加载器,这个委派加载的处理过程会递归进行。
如果父加载器最终没有找到该类,子加载器将调用 java.net.URLClassLoader.findClass() 方法从文件系统中加载该类。如果最终子加载器也无法加载该类,将抛出 java.lang.NoClassDefFoundError 或 java.lang.ClassNotFoundException。
让我们看一个抛出 ClassNotFoundException 的例子:
java.lang.ClassNotFoundException: com.baeldung.classloader.SampleClassLoader
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:348)
如果我们从调用 java.lang.class.forName() 开始按顺序看上面代码,可以发现首先会通过父加载器加载类,然后子加载器通过 java.net.URLClassLoader.findClass() 再进行查找,最后抛出了 ClassNotFoundException。
Class Loaders有三个很重要的特性。
委派模型(Delegation Model)
当需要查找class或resource时,Class loaders会遵守委派模型,它们首先会将查找请求委派给其父加载器。
假设我们需要将应用中的一个类加载到JVM,system class loader首先会将加载请求委派给extension class loader,后者又会将加载请求委派给bootstrap class loader。
只有当bootstrap class loader和extension class loader都无法加载该类时,system class loader才会尝试自行加载该类。
唯一性(Unique Classes)
作为委派模型的结果,我们总是尝试向上委托,因此很容易保证类的唯一性。如果父加载器无法找到该类,当前加载器才会尝试加载该类。
可见性(Visibility)
此外,父加载器加载的类对子加载器是可见的。
举个例子,system class loader可以看到extension class loader和bootstrap class loader加载的类,但是反之不行,父加载器无法看到子加载器加载的类。
为了说明这一点,假如类A由system class loader加载,类B由extension class loader加载,那么A和B对于对于system class loader来说都是可见的,extension class loader只能看到类B。
自定义ClassLoader
对于文件系统中的文件来说,内置class loader已经可以满足大部分场景。然而,有些场景并不是从本机硬件设备或网络上加载类,因此我们需要自定义class loader来处理。
在本小节,我们将介绍自定义加载器的一些场景,也会介绍如何创建一个自定义加载器。
自定义classloader的场景
自定义classloader不仅仅只用于在运行时加载类,还有这么一些场景:
- 用于更新已存在的字节码,如:编织代理(weaving agent)。
- 根据需求动态创建类,如:在JDBC中通过加载类来完成不同驱动程序之间的切换。
- 在加载具有相同类名、包名的类的字节码时实现类的版本控制机制,可以通过URL类加载器(通过URL加载jar)或自定义加载器。
还有很多自定义加载器可以派上用场的例子。
例如,浏览器使用自定义加载器从网站加载可执行的内容。浏览器可以使用独立的class loader从不同网页加载applet,用于运行applet的applet查看器包含了一个ClassLoader,它不从本地文件系统检索类,而是访问远程服务器上的站点。然后通过HTTP加载字节码原文件,并将其转换为JVM中的类。虽然这些applet具有相同的名称,但由于它们被不同的class loader所加载,因此它们也被看作不同的组件。
现在我们理解了自定义加载器的意义,那就让我们实现一个ClassLoader的子类来总结JVM类的加载。
创建我们自己的class loader
为了便于说明,假设我们需要通过FTP加载类。由于类不在classpath中,无法通过内置加载器加载这些类。
public class CustomClassLoader extends ClassLoader {
public CustomClassLoader(ClassLoader parent) {
super(parent);
}
public Class getClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFTP(name);
return defineClass(name, b, 0, b.length);
}
@Override
public Class loadClass(String name) throws ClassNotFoundException {
if (name.startsWith("com.baeldung")) {
System.out.println("Loading Class from Custom Class Loader");
return getClass(name);
}
return super.loadClass(name);
}
private byte[] loadClassFromFTP(String fileName) {
// Returns a byte array from specified file.
}
}
在上面例子中,我们定义了一个class loader用于从包 com.baeldung 加载文件,拓展了默认class loader。
我们在构造器中传入了parent class loader,然后使用类的全限定名通过FTP加载类。
理解java.lang.ClassLoader
让我们了解下 java.lang.ClassLoader 中的几个基础方法,以便对ClassLoader的工作方式有个清晰的脑图。
loadClass()方法
public Class> loadClass(String name, boolean resolve) throws ClassNotFoundException {
该方法通过给定的类全限定名加载类,如果参数 resolve 为 true,JVM将执行 loadClass() 解析该类。然而,我们并非总是需要解析一个类。 如果只需要判断类是否存在,可以将 resolve参数设置为false。
这个方法是class loader的入口,我们可以通过源码了解 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.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
方法中查找类的默认实现按照如下顺序进行:
- 执行 findLoadedClass(String) 判断类是否已被加载
- 执行父类的 loadClass(String) 方法
- 执行 findClass(String) 方法查找类
defineClass()方法
protected final Class> defineClass(
String name, byte[] b, int off, int len) throws ClassFormatError
该方法负责将字节数组转换为类,我们需要在使用类之前先解析类。
如果没有包含有效的类,将抛出 ClassFormatError。
当然,该方法由 final标记,我们不能override。
findClass()方法
protected Class> findClass(
String name) throws ClassNotFoundException
该方法以类全限定名来查找类,在自定义的class loader中,我们需要override这个方法,并且需要遵守委派模型。
当然,如果父加载器无法找到目标类,将会执行 loadClass() 方法。
在默认实现中,如果所有父加载器都无法查找到该类,将抛出 ClassNotFoundException。
getParent()方法
这个方法返回父加载器用于委派。
有些实现像最上面例子中使用 null 来代表bootstrap class loader。
getResource()方法
public URL getResource(String name)
该方法用于查找给定名称的资源。
首先,查找请求会委托给父加载器。如果父加载器为null,则将请求交给bootstrap class loader。
如果依然失败,该方法将调用 findResource(String) 来查找资源。它返回一个用于读取资源的URL对象,如果没有找到资源或没有足够的权限访问资源将返回 null。
值得注意的是,Java会从classpath路径中加载资源。
线程上下文加载器(Context Classloaders)
Context Classloaders为J2SE中引入的类加载委派方案提供了另一种方式。
前面我们学到,JVM中的class loaders遵循层级模型,除bootstrap class loader外,每个类加载器都有一个父类。
然而,有时当JVM核心类需要加载由开发人员提供的类或资源时,我们可能会遇到问题。
例如,在JNDI中,其核心功能由 rt.jar 中的引导类实现。但是这些JNDI引导类可能需要加载由各独立服务商提供的JNDI实现类(部署在应用的classpath中),这个场景需要bootstrap class loader加载一些仅对child class loader可见的类。
J2SE委派在这里并不管用,我们需要找到一种替代方法来加载类。这可以使用线程上下文加载器来实现。
java.lang.Thread 类有一个 getContextClassLoader 方法用于返回特定线程的ContextClassLoader。在加载资源和类时,ContextClassLoader由线程的创建者提供。
小结
Class loaders是执行Java程序的基础,本文我们进行了简单介绍。
我们介绍了几种不同的class loader——Bootstrap,Extension和System class loaders。Bootstrap作为所有class loader的父类,负责加载JDK核心类。Extension和System负责加载Java拓展目录和classpath中的类。
然后,我们介绍了class loader的工作方式,通过创建一个简单的自定义class loader介绍了几个特性,如:委派、可见性和唯一性。最后,我们简介了 Context class loaders。
本文的样例代码见 github。