类加载器的作用

如果你了解jvm的类加载机制,就会知道类加载的几个步骤:加载、验证、准备、解析、初始化、使用和卸载。
其中,涉及到类加载器的就是第一个阶段:加载阶段。
《深入理解java虚拟机》中有这样的描述:
在加载阶段,虚拟机需要完成下面三件事情:
(1)通过一个类的全限定名来获取此类的二进制字节流。
(2)将这个字节流代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
不知道读到这里,你们会不会有疑问:如果代表这个类的java.lang.Class对象在类加载的第一个阶段也就是加载阶段就在内存中创建出来了,那么是不是对于某个类来说,只有这一个代表这个类的Class对象实例?或者这样说,不管我用什么方式获取某个类的Class对象,返回的都是加载阶段生成的这个Class的对象?这个可以通过代码验证:

  1. public class SimpleObject {
  2. public static void main(String[] args) throws ClassNotFoundException {
  3. SimpleObject simpleObject = new SimpleObject();
  4. SimpleObject simpleObject1 = new SimpleObject();
  5. System.out.println(simpleObject.getClass().equals(simpleObject1.getClass()));
  6. System.out.println(simpleObject.getClass().equals(SimpleObject.class));
  7. System.out.println(Class.forName("person.andy.concurrency.classload.SimpleObject").equals(simpleObject.getClass()));
  8. }
  9. }

上面的代码中,我通过三种方式获取了SimpleObject的Class对象,分别是Class.forName(“类的全限定名”),类对象.getClass()和类名.class,并且第二种方式使用了两个不同的类实例对象,运行结果会输出三个true。
这就证明了上面我们的观点:在加载阶段生成的代表某个类的Class对象,是这个类在内存中的唯一的Class对象,不论我们以哪种方式获取,返回的都是这个Class对象实例。其实这也是很合理的,因为一个类对应一个Class,而这个Class当然没必要有多个实例。
上面说的这个问题,更应该归纳到Class的知识体系中,不过,既然我们讲到了加载阶段,那跟这个阶段关系密切的就是类加载器,所以在这里描述这个问题,就当是对疑问的一个解答吧。
现在我们回到类加载器,类加载器到底是做什么的?我在IBM developer的一个对类加载器进行讲解的文章中找到了这样的描述:
一般来说,Java 虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取 Java 字节代码,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个 Java 类。通过此实例的 newInstance() 方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
这段描述和《深入理解java虚拟机》中对加载阶段的描述很类似,就是获取class文件,然后生成Class实例。 实际上,类加载器的作用不止这些,对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。每一个类加载器,都有一个独立的类名称空间。也就是说,比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

类加载器的分类

关于类加载器的分类,这里主要参考《深入理解java虚拟机》中的描述:
从Java开发人员的角度来看,绝大部分Java程序都会使用到以下3种系统提供的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在\lib目录中的,或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
  • 扩展类加载器(Exteension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。他负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中的默认类加载器。可以看到,扩展类加载器和应用程序类加载器都是sun.misc.Launcher类的内部类。

除了启动类加载器外,所有的类加载器都有一个父类加载器。通过getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器就是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器。
根据上面的分类,我们就可以引申出一些简单直观的测试,首先看下面的代码:

  1. public class ClassLoadTest {
  2. public static void main(String[] args) {
  3. ClassLoader classLoader = ClassLoadTest.class.getClassLoader();
  4. System.out.println(classLoader);
  5. ClassLoader parent1 = classLoader.getParent();
  6. System.out.println(parent1);
  7. ClassLoader parent2 = parent1.getParent();
  8. System.out.println(parent2);
  9. }
  10. }
  1. sun.misc.Launcher$AppClassLoader@18b4aac2
  2. sun.misc.Launcher$ExtClassLoader@511d50c0
  3. null

在这个示例中,我们在当前的ClassPath中定义了一个类ClassLoadTest,这个类肯定是由AppClassLoader来加载,然后我们调用了这个classLoader的getParent()方法,输出的是ExtClassLoader。然后继续调用ExtClassLoader的getParent()方法,返回的是null。ExtClassLoader的parent是Bootstrap加载器,而Bootstrap类加载器是jvm的一部分,所以这里输出的是null。

还有一个需要注意的是,数组类本身不通过类加载器创建,它是由Java虚拟机直接创建的。但数组类与类加载器仍然有密切的关系,因为数组类的元素类型(指的是数组去掉所有纬度的类型)最终是靠类加载器去创建,一个数组类的创建过程遵循以下规则(根据《深入理解java虚拟机》的描述):

  • 如果数组的组件类型(指的是数组去掉一个维度的类型)是引用类型,那么就递归采用本节中定义的加载过程去加载这个组件类型,数组C将在加载该组件类型的类加载器的类名称空间上被标识。
  • 如果数组的类型不是引用类型,例如int[]数组,Java虚拟机将会把数组C标记为与引导类加载器关联。
  • 数组类的可见性与它的组件类型的可见性一直,如果组件类型不是引用类型,那数组类的可见性将默认为public。

但是在ClassLoader类的注释中,有这样的描述:

  1. Class objects for array classes are not created by class loaders,
  2. but are created automatically as required by the Java runtime.
  3. The class loader for an array class, as returned by
  4. {@link Class#getClassLoader()} is the same as the class loader for its element type;
  5. if the element type is a primitive type, then the array class has no class loader.

意思是,一个数组类的class loader,与它的元素类型(去掉所有维度之后的元素)的class Loader相同,如果元素类型是基础类型,那么这个array class没有class loader。
这两种描述针对简单类型的数组的classLoader会有些出入,一个说是与引导类加载器关联,一个说没有类加载器,但不论是哪种描述,在对这类数组调用getClassLoader()方法时,都会返回null;
看下面的例子:

  1. public class ClassLoadTest {
  2. public static void main(String[] args) {
  3. ClassLoadTest[] classLoadTests = new ClassLoadTest[3];
  4. System.out.println(classLoadTests.getClass().getClassLoader());
  5. String[] strings = new String[3];
  6. System.out.println(strings.getClass().getClassLoader());
  7. int[] ints = new int[3];
  8. System.out.println(ints.getClass().getClassLoader());
  9. }
  10. }

输出结果如下:

  1. sun.misc.Launcher$AppClassLoader@18b4aac2
  2. null
  3. null

第一个是我们自定义的ClassLoadTest的数组,元素类型是ClassLoadTest类,这个类是通过AppClassLoader加载的,所以输出的是sun.misc.Launcher$AppClassLoader@18b4aac2。
第二个数组是String的数组,由于String是在/lib下的类,所以是引导类加载器来加载的,引导类加载器会输出null;
第三个是int数组,元素类型是原始类型,原始类型的类加载器同样是引导类加载器,所以输出为null;

双亲委派模型

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,如果当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。关于这个逻辑在代码中的具体实现,后面在源码分析阶段会有讲解。

ClassLoader源码分析

下图中给出了ClassLoader类中与加载类相关的方法:

classLoader.png
我们现在就重点分析一下ClassLoader中的这几个方法:

loadClass方法

loadClass方法也是双亲委派模型在代码中的体现,在抽象类ClassLoader中有两个loadClass方法:

  1. public Class<?> loadClass(String name) throws ClassNotFoundException {
  2. return loadClass(name, false);
  3. }

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException

第一个是公有访问权限的,第二个只有子类才能访问。从代码中可以看到,第一个实际上是调用了第二个,只是resolve参数总是false,也就是说,外部类可以访问loadClass(String)来实现类的加载。我们重点来看第二个loadClass方法的实现:
首先看源码中的注释:

  1. Loads the class with the specified <a href="#name">binary name</a>. The
  2. default implementation of this method searches for classes in the
  3. following order:
  4. 根据指定的二进制名称来加载class,默认的查找类的顺序如下:
  5. Invoke {@link #findLoadedClass(String)} to check if the class has already been loaded.
  6. 1)执行findLoadedClass方法来检查这个类是否已经被加载过。
  7. Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method on the parent class loader.
  8. If the parent is <tt>null</tt> the class loader built-in to the virtual machine is used, instead
  9. 2)执行父类加载器的loadClass方法,如果父类加载器为null,就使用内置的类加载器,也就是引导类加载器。
  10. Invoke the {@link #findClass(String)} method to find the class.
  11. 3)执行当前类的findClass
  12. Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
  13. #findClass(String)}, rather than this method.
  14. ClassLoader的子类推荐重写findClass方法而不是loadClass方法。

代码如下

  1. protected Class<?> loadClass(String name, boolean resolve)
  2. throws ClassNotFoundException
  3. {
  4. synchronized (getClassLoadingLock(name)) {
  5. // First, check if the class has already been loaded
  6. Class<?> c = findLoadedClass(name);
  7. if (c == null) {
  8. long t0 = System.nanoTime();
  9. try {
  10. if (parent != null) {
  11. c = parent.loadClass(name, false);
  12. } else {
  13. c = findBootstrapClassOrNull(name);
  14. }
  15. } catch (ClassNotFoundException e) {
  16. // ClassNotFoundException thrown if class not found
  17. // from the non-null parent class loader
  18. }
  19. if (c == null) {
  20. // If still not found, then invoke findClass in order
  21. // to find the class.
  22. long t1 = System.nanoTime();
  23. c = findClass(name);
  24. }
  25. }
  26. if (resolve) {
  27. resolveClass(c);
  28. }
  29. return c;
  30. }
  31. }

首先,调用findLoadedClass方法查看该类是否被加载过,如果有,直接返回该Class对象,这个findLoadedClass方法是native方法,我们无法查看它的实现逻辑。如果没被加载过,就判断当前classLoader的parent是否存在,如果存在,就调用父类加载器的loadClass方法来加载该类,也就是这句代码:

  1. c = parent.loadClass(name, false);

如果没有父类加载器,就使用引导类加载器:

  1. c = findBootstrapClassOrNull(name);
  1. private Class<?> findBootstrapClassOrNull(String name)
  2. {
  3. if (!checkName(name)) return null;
  4. return findBootstrapClass(name);
  5. }

findBootstrapClassOrNull方法调用的是findBootstrapClass方法,这个方法同样是一个native方法。

  1. private native Class<?> findBootstrapClass(String name);

如果使用父类加载器或者引导类加载器还是没能加载类(抛出了ClassNotFoundException这个异常意味着没有找到),就需要当前类加载器自己去加载了。

  1. if (c == null) {
  2. // If still not found, then invoke findClass in order
  3. // to find the class.
  4. long t1 = System.nanoTime();
  5. c = findClass(name);
  6. }

注意这里调用的是findClass方法去查找类。
所以类加载器的双亲委派模型是在loadClass中实现的,这也是为什么注释中建议ClassLoader的子类重写findClass方法而不是loadClass方法,因为重写loadClass方法可能会破坏双亲委派模型。

findClass

在抽象类ClassLoader中的findClass只是抛出了一个异常。

  1. protected Class<?> findClass(String name) throws ClassNotFoundException {
  2. throw new ClassNotFoundException(name);
  3. }

这个是由子类自己去实现的。

上面说的loadClass和findClass两个方法,都会抛出ClassNotFoundException,我们这里研究一下这个异常。
这个异常的定义如下:

  1. public class ClassNotFoundException extends ReflectiveOperationException {}

实现了ReflectiveOperationException,从名字上看ReflectiveOperationException,大概是跟反射相关的,看定义和注释:

  1. /**
  2. * Common superclass of exceptions thrown by reflective operations in
  3. * core reflection.
  4. *
  5. * @see LinkageError
  6. * @since 1.7
  7. */
  8. public class ReflectiveOperationException extends Exception {}

果然,是所有在反射核心包中的反射操作异常的父类。
再来看ClassNotFoundException的注释:

  1. /**
  2. * Thrown when an application tries to load in a class through its
  3. * string name using:
  4. * <ul>
  5. * <li>The <code>forName</code> method in class <code>Class</code>.
  6. * <li>The <code>findSystemClass</code> method in class
  7. * <code>ClassLoader</code> .
  8. * <li>The <code>loadClass</code> method in class <code>ClassLoader</code>.
  9. * </ul>
  10. * <p>
  11. * but no definition for the class with the specified name could be found.
  12. *
  13. * <p>As of release 1.4, this exception has been retrofitted to conform to
  14. * the general purpose exception-chaining mechanism. The "optional exception
  15. * that was raised while loading the class" that may be provided at
  16. * construction time and accessed via the {@link #getException()} method is
  17. * now known as the <i>cause</i>, and may be accessed via the {@link
  18. * Throwable#getCause()} method, as well as the aforementioned "legacy method."

注释写的很清晰,当我们调用Class.forName() ClassLoader.findSystemClass和ClassLoader.loadClass这三个方法时,如果根据类的name没有找到这个类的定义,就会抛出这个异常。在我们编译java工程的时候,会经常碰到这个异常,但是这个异常并不是在编译期才能产生,上面的三个方法调用,就能使的这个异常在运行期产生。

defineClass

在ClassLoader的代码中可以看到六个defineClass方法:

  1. protected final Class<?> defineClass(String name, byte[] b, int off, int len)
  2. throws ClassFormatError
  3. {
  4. return defineClass(name, b, off, len, null);
  5. }
  6. protected final Class<?> defineClass(String name, byte[] b, int off, int len,
  7. ProtectionDomain protectionDomain)
  8. throws ClassFormatError
  9. protected final Class<?> defineClass(String name, java.nio.ByteBuffer b,
  10. ProtectionDomain protectionDomain)
  11. throws ClassFormatError
  12. private native Class<?> defineClass0(String name, byte[] b, int off, int len,
  13. ProtectionDomain pd);
  14. private native Class<?> defineClass1(String name, byte[] b, int off, int len,
  15. ProtectionDomain pd, String source);
  16. private native Class<?> defineClass2(String name, java.nio.ByteBuffer b,
  17. int off, int len, ProtectionDomain pd,
  18. String source);

其中,defineClass0、defineClass1和defineClass2这三个是native方法。第一个defineClass调用了第二个defineClass,也就是说,实际上就是第二个和第三个是需要我们去分析的。
先看这个方法:

  1. protected final Class<?> defineClass(String name, byte[] b, int off, int len,
  2. ProtectionDomain protectionDomain)
  3. throws ClassFormatError
  4. {
  5. protectionDomain = preDefineClass(name, protectionDomain);
  6. String source = defineClassSourceLocation(protectionDomain);
  7. Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
  8. postDefineClass(c, protectionDomain);
  9. return c;
  10. }

我们先不去看里面都调用了什么方法,注释里面有两个需要注意的Error。

  1. * @throws ClassFormatError
  2. * If the data did not contain a valid class
  3. *
  4. * @throws NoClassDefFoundError
  5. * If <tt>name</tt> is not equal to the <a href="#name">binary
  6. * name</a> of the class specified by <tt>b</tt>
  7. *

注意这两个异常,一个是ClassFormatError,一个是NoClassDefFoundError,我们先来看一下这两个Error。

  1. /**
  2. * Thrown when the Java Virtual Machine attempts to read a class
  3. * file and determines that the file is malformed or otherwise cannot
  4. * be interpreted as a class file.
  5. *
  6. * @author unascribed
  7. * @since JDK1.0
  8. */
  9. public
  10. class ClassFormatError extends LinkageError {}

ClassFormatError继承了LinkageError,根据注释,在jvm尝试读取一个class文件并且认为这个文件不合法或者因为其他原因不能被解释的时候,就会抛出这个error。

  1. /**
  2. * Thrown if the Java Virtual Machine or a <code>ClassLoader</code> instance
  3. * tries to load in the definition of a class (as part of a normal method call
  4. * or as part of creating a new instance using the <code>new</code> expression)
  5. * and no definition of the class could be found.
  6. * <p>
  7. * The searched-for class definition existed when the currently
  8. * executing class was compiled, but the definition can no longer be
  9. * found.
  10. *
  11. * @author unascribed
  12. * @since JDK1.0
  13. */
  14. public
  15. class NoClassDefFoundError extends LinkageError {}

NoClassDefFoundError同样是LinkageError的子类,根据注释,当jvm或者ClassLoader尝试去加载一个类的定义(正常的方法调用或者new关键字表达式)但是不能找到的时候,就会抛出这个Error。类的定义已经在class编译的时候存在了但是定义不能被找到。
NoClassDefFoundError是运行时错误而不是编译期异常。

加载类的过程

在前面介绍类加载器的代理模式的时候,提到过类加载器会首先代理给其它类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass 来实现的;而启动类的加载过程是通过调用 loadClass 来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在 Java 虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer 引用了类 com.example.Inner ,则由类 com.example.Outer 的定义加载器负责启动类 com.example.Inner 的加载过程。
方法 loadClass() 抛出的是 java.lang.ClassNotFoundException 异常;方法 defineClass() 抛出的是 java.lang.NoClassDefFoundError 异常。
类加载器在成功加载某个类之后,会把得到的 java.lang.Class 类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass 方法不会被重复调用。

线程上下文类加载器

线程上下文类加载器是从JDK1.2开始引入的。类java.lang.Thread中的方法getContextClassLoader()和setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。