【Using English】57 Class Loaders
1. 介绍类加载器
类加载器负责在运行时动态地加载Java类到Java虚拟机。当然,类加载器也是Java运行时环境的一部分。因此,Java虚拟机并不需要了解底层的文件或文件系统就可以运行Java程序了,能够做到这一点多亏了类加载器(们)。
同时,这些类加载器也不是一次性全部加载到内存,而是应用程序需要的时候加载。这就是类加载器发挥作用的地方,负责加载类到内存。
本次教程,我们将要讨论内建的几种类加载器,它们如何工作以及介绍我们自定义的类加载器实现。>
延伸阅读
理解Java内存泄漏:学习Java内存泄漏,如何在运行时识别内存泄漏,造成内存泄漏的原因,以及避免的方案。
2. 内建类加载器的类型
用简单例子学习不同的类如何被不同的类加载器加载的:
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
正如我们看到的,有三种不同的类加载器;应用类加载器,扩展类加载器,以及启动类加载器(显示为null)
应用类加载器加载了包含了示例方法的类,应用或者系统的类加载器加载了文件路径中的我们自己的文件。
接下来,扩展类加载器加载了Logging日志类。扩展类加载器加载了标准Java核心类
最后,启动类加载器加载了ArrayList类,启动类加载器或者初始类加载器是所有其他加载器的双亲(单亲?)
但是,我们看到最后一个输出,ArrayList的类加载器输出一个null。这是因为启动类加载器是用native代码的写的,不是Java,所以没能显示成一个Java类的形式。因为这个原因,启动类加载器的行为会在不同的JVM上有所差异。
我们更详细地讨论一下每一个类加载器。
2.1. 启动类加载器
Java类会被类加载器java.lang.ClassLoader
的一个实例加载。但是类加载器本身也是一个类。那么问题来了,谁来加载类加载器java.lang.ClassLoader
(这个类)?
这就是启动类加载或者首要类加载器发挥作用的地方。
启动类加载器主要负责加载JDK内部的类,典型地是rt.jar
和其他位于$JAVA_HOME/jre/lib
目录的核心类库。另外,启动类加载器作为所有类加载器实例的父加载器提供服务。
启动类加载器本身是Java虚拟机的一部分,使用native代码写的就像上面例子中支出的那样。对于这个特殊的类加载器,不同的平台可能有不同的实现。
2.2. 扩展类加载器
扩展类加载器是启动类加载器的一个子类,负责加载Java标准核心库的扩展,所以可以触达所有运行在平台上的应用。
扩展类加载器加载JDK扩展的目录,通常是$JAVA_HOME/lib/ext
或者其他在java.ext.dirs
中配置了系统属性的目录。
2.3. 系统类加载器
系统类加载器或者叫应用类加载器,一方面负责把所有应用层的类加载到Java虚拟机。应用类加载器也负责加载类路径环境变量中的文件, -classpath或者-cp命令行选项**。另外,应用类加载器也是扩展类加载器的子类。
3. 类加载器是如何工作的?
类加载器是Java运行时环境的一部分。当Java虚拟机请求一个类的时候, 类加载器会尝试定位那个类,然后使用全限定类名来加载类定义到运行时环境。
java.lang.ClassLoader.loadClass()
方法负责加载类定义到运行时,它会尝试加载基于全限定名的类。
如果类还未被加载,它(loadClass方法)会代理请求到父加载器,这个过程会递归进行。
最后,如果父加载器没有找到类,然后子加载器会调用java.net.URLClassLoader.findClass()
方法在自己的文件系统中搜索这个类。如果最后一个子类加载器也没能加载这个类,加载器会抛出java.lang.NoClassDefFoundError
或者java.lang.ClassNotFoundException
Let's look at an example of output when ClassNotFoundException is thrown.
我们看一个抛出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
异常。
类加载机制有3个重要的特性:
3.1. 代理模型
类加载器遵循代理模式,每当需要寻找一个类或者资源时,一个类加载器实例会让父加载器代理搜索类或者资源。
比如说,有一个加载应用层类到Java虚拟机的请求。系统类加载器(应用类加载器)首先让它的父加载器(也就是扩展类加载器)代理加载这个类,而扩展类加载器顺手就丢给了启动类加载器去加载。
只有启动类加载器以及扩展类加载器都加载失败了,系统类加载器才会尝试自己加载这个类。
3.2. 类的唯一性
作为代理模式的结果,由于我们总是尝试向上代理,很容易就能到处类的唯一性的特性。
如果父加载器没能找到目标类,这个时候当前的类加载器实例才会试图自己加载。(这一点保证了类的唯一性)
3.3. 类的可见性
另外,子加载器可以访问被父加载器加载的类。
例如,系统类加载器加载的类可以访问扩展类加载器和启动类加载器加载的类,反之不成立(启动类加载器加载的类不能访问扩展类加载器和系统类加载器加载的类,扩展类加载器加载的类不能访问系统类加载器加载的类)。
为了说明这一点,如果类A被应用类加载器加载,类B被扩展类加载器加载;那么类A与类B都可以被应用类加载器加载的类访问。
但是,被扩展类加载器加载的类的范围内,就只能访问到类B。
4. 自定义类加载器
内建的类加载器可以满足大多数文件已经在文件系统的情况。
但是,在需要加载的类不在本地磁盘或者来自网络的场景,我们可能需要实现自己的类加载器。
这一部分,我们将会覆盖一些自定义类加载器的其他使用场景,也会展示如何创建它。
4.1. 自定义类加载器的使用场景
除了运行时加载类,自定义类加载器也可以在以下场景发挥作用:
- 帮助修改已经存在的字节码,例如:编织代理
- 动态创建类可以满足一些使用者的需求。比如,在JDBC中,在不同驱动实现中切换这个需求,可以通过动态加载类来实现。
- 实现版本控制机制,当需要为相同类名和包名的类加载不同字节码时,这个需求可以通过URL类加载器(通过URL加载jar文件)或者自定义类加载器实现。
有更多具体的例子,自定义类加载器迟早会派上用场。
比如,浏览器使用一个自定义的类加载器来加载网站上的可执行内容。浏览器可以使用不同的类加载器来加载来自不同网页的小程序(Applet)。可以运行小程序的查看器包含了一个类加载器,这类加载器可以从远程服务器拿到网页内容,而不是在本地的文件系统 (哦, 原来这就是Java的小程序。)。
然后通过HTTP协议加载原始的字节码文件,并且把它们转换成类加载到Java虚拟机中。如果被不同的类加载器加载,即使这些小程序有着同样的名字也会被识别为不同的组件。(不同的加载器可以看成是不同的命名空间)
既然我们理解了自定义类加载器非常重要,我们一起实现一个ClassLoader
的子类来扩展和汇总Java虚拟机加载类的功能。
4.2. Creating our Custom Class Loader
为了演示的目的,比如说我们需要使用自定义的类加载器从文件里加载一个类。
我们需要继承ClassLoader
类,并且重写findClass()
方法
public class CustomClassLoader extends ClassLoader {
@Override
public Class findClass(String name) throws ClassNotFoundException {
byte[] b = loadClassFromFile(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassFromFile(String fileName) {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(
fileName.replace('.', File.separatorChar) + ".class");
byte[] buffer;
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
int nextValue = 0;
try {
while ( (nextValue = inputStream.read()) != -1 ) {
byteStream.write(nextValue);
}
} catch (IOException e) {
e.printStackTrace();
}
buffer = byteStream.toByteArray();
return buffer;
}
}
上面的例子中,我们自定义了一个类加载器,它继承了默认的类加载器并且从一个具体的文件中加载了一个字节数组。
5. 理解 java.lang.ClassLoader
我们讨论几个来自java.lang.ClassLoader
的必要方法,这样就更清楚它是如何工作的。
5.1. The loadClass() Method
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
这个方法通过给定一个名字的参数来加载一个类,这个名字的参数是全限定类名。
resolve
设置为true
,Java虚拟机会执行loadClass()
方法来解析类的引用。然而,解析一个类并不总是必要的。如果我们只需要确定类是否存在,那么解析resolve
参数可以设为false
这个方法是类加载器的入口点。
我们可以尝试查看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.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
该方法的默认实现以下面的顺序查找类:
- 调用
findLoadedClass(String)
方法来查看该类是否已经加载 - 调用父加载器的
loadClass(String)
方法 - 调用
findClass(String)
方法来查找类
5.2. The defineClass() Method
protected final Class<?> defineClass(
String name, byte[] b, int off, int len) throws ClassFormatError
这个方法负责把一个字节数组转换成一个类的实例对象。在我们使用类之前,我们需要解析它。
如果数据未包含一个合法的类,抛出ClassFormatError
异常。
当然,我们不能复写这个方法,因为它是final
的。
5.3. The findClass() Method
protected Class<?> findClass(
String name) throws ClassNotFoundException
这个方法把全限定类名作为一个参数来查找类。以遵循代理机制来实现的自定义类加载器中,我们需要复写这方法。
当然,如果父加载器没能找到目标类,loadClass()
也会执行这个方法。
如果没有父加载器可以找到这个类,默认的实现抛出一个ClassNotFoundException
异常。
5.4. The getParent() Method
public final ClassLoader getParent()
这个方法返回了用来代理的父加载器。
如第二部分的显示,一些实现方法使用null
来代表启动类加载器。
5.5. The getResource() Method
public URL getResource(String name)
这个方法尝试使用给定的名称参数寻找一个资源。
首先会代理给父加载器来寻找资源,如果父加载器是null
, 内建在虚拟机中的类加载器的路径将会被查询。
如果上面的步骤失败了,getResource(String)
方法将会调用findResource(String)
方法来查找资源。资源名称可以是指向类路径的相对或者绝对的特定输入。
方法会返回一个URL
来读取资源,或者在找不到资源或无权限时返回null
。
必须强调一下:Java从类路径(classpath)加载资源。
最后,Java中的资源加载是与路径无关的,因为无论代码在哪里运行,只要环境设置好就可以找到资源。
6. 上下文类加载器
通常,作为一种类加载方案中的可选择的方法,上下文类加载器被引入了J2SE。
就像我们之前学到的那样,Java虚拟机中的类加载器遵循了集成的模型这样每一个类加载器会有一个单独的父加载器,除了启动类加载器。
但是,有时候但Java虚拟机核心类需要动态加载由应用开发者提供的类或资源时,我们或许会遇到一个问题。
例如,在JNDI
中,核心方法被启动类加载器实现在了rt.jar
中,但是这些JNDI
类或者会加载JNDI
被独立供应商实现的提供者,这些供应商发布在应用的类路径中。这种场景导致了启动类加载器加载一个应用类加载器才能访问的类。
J2ME
代理并不在这里工作,也不解决这个问题,我们需要寻找可以替代的类加载方案,这个问题可以用现成上下文类加载器来解决。
java.lang.Thread
类有一个方法getContextClassLoader()
, 这个方法为每个线程返回一个ContextClassLoader
类加载器实例。当加载资源和和类时,ContextClassLoader
被现成的创建者提供。
如果未设定值,那么默认使用父线程的上下文类加载器。
7. 结论
类加载器执行Java程序的必要部分。它作为文章的一部分,我们已经介绍了挺多内容。
我们讨论了不同类型的类加载器,启动类加载器、扩展类加载器和系统类加载器。启动类加载器做为所有类加载的附加载体提供服务。它负责加载JDK内部的类。扩展类加载器和系统类加载器,分别加载了来自Java扩展和类路径里的类。
然后我们讨论了类加载器是如何工作的,我们也讨论了一些特性比如代理机制,类的可见性和唯一性。然后简单地实现了自定义类加载器。最后,我们介绍了一下上下文类加载器。
像往常一样,代码Sample可以在Github找到。