类加载器基本概念
顾名思义,类加载器(ClassLoader)用来加载Java类的字节码到Java虚拟机中,并转换成 java.lang.Class 类的一个实例。每个这样的实例用来表示一个Java类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。实际的情况可能更加复杂,比如 Java 字节代码可能是通过工具动态生成的,也可能是通过网络下载的。
但我们执行java.exe程序时,它首先会定位使用的JRE,然后激活JRE中的JVM。JVM首先会做一些初始化工作,然后或初始化类加载器,并形成如下图的类加载器体系结构:
如上图所示:BootstrapLoader(成为引导类加载器),首先会搜索sun.boot.class.path系统参数所指定的路径,并加载其中的类,然后BootstrapLoader会加载ExtClassLoader和AppClassLoader,并启动他们,同时设定它们parent classloader,随后ExtClassLoader会搜索java.ext.dirs系统参数指定路径,并加载其中的类,同时AppClassLoader会搜索java.class.path系统参数指定的路径并加载其中类,完成这写步骤后就形成了类加载器体系结构。
实例代码:
public class Path{ public static void main(String[] args){ String bootPath = System.getProperty("sun.boot.class.path"); System.out.println("sun.boot.class.path: "); printPath(bootPath); String extPath = System.getProperty("java.ext.dirs"); System.out.println("java.ext.dirs: "); printPath(extPath); String classPath = System.getProperty("java.class.path"); System.out.println("java.class.path: "); printPath(classPath); ClassLoader loader = Path.class.getClassLoader(); while(loader != null){ System.out.println(loader); loader = loader.getParent(); } } public static void printPath(String path){ String[] paths = path.split(";"); for(String temp: paths){ System.out.println(temp); } } }
本机运行结果:
sun.boot.class.path: C:\Java\jdk1.7.0_17\jre\lib\resources.jar C:\Java\jdk1.7.0_17\jre\lib\rt.jar C:\Java\jdk1.7.0_17\jre\lib\sunrsasign.jar C:\Java\jdk1.7.0_17\jre\lib\jsse.jar C:\Java\jdk1.7.0_17\jre\lib\jce.jar C:\Java\jdk1.7.0_17\jre\lib\charsets.jar C:\Java\jdk1.7.0_17\jre\lib\jfr.jar C:\Java\jdk1.7.0_17\jre\classes java.ext.dirs: C:\Java\jdk1.7.0_17\jre\lib\ext C:\Windows\Sun\Java\lib\ext java.class.path: . ************classLoaderTree************ sun.misc.Launcher$AppClassLoader@fb56b1 sun.misc.Launcher$ExtClassLoader@f8968f
之所以会存在类加载器的层次体系,是因为要实现所谓的装载委托模型。所谓的委托模型是指,在类加载器加载类时,首先强求其parent类加载器在它的搜索路径中加载指定的类,如果parent类加载器找不到,则由自己加载,这样做一方面为了动态,一方面为了安全。因为在次模型下,用户自定义的类加载器不可能加载应该由parent类加载器加载的类,从而防止不可靠甚至恶意代码代替由parent类加载器加载的可靠的代码。不同的类加载器具有隔离性,这就使得同一个类在内存可能出现2个Class类的实例,但位于不同的命名空间。
在java中自定义的类加载器都继承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; } }
加载类的过程
在前面介绍类加载器的装载委托的时候,提到过类加载器会首先代理给其parent类加载器来尝试加载某个类。这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 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 方法不会被重复调用。
下面讨论另外一种类加载器:线程上下文类加载器。
线程上下文类加载器
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread 中的方法 getContextClassLoader() 和 setContextClassLoader(ClassLoader cl) 用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl) 方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
前面提到的类加载器的代理模式并不能解决 Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。这些 SPI 的接口由 Java 核心库来提供,如 JAXP 的 SPI 接口定义包含在 javax.xml.parsers 包中。这些 SPI 的实现代码很可能是作为 Java 应用所依赖的 jar 包被包含进来,可以通过类路径(CLASSPATH)来找到,如实现了 JAXP SPI 的 Apache Xerces 所包含的 jar 包。SPI 接口中的代码经常需要加载具体的实现类。如 JAXP 中的 javax.xml.parsers.DocumentBuilderFactory 类中的 newInstance() 方法用来生成一个新的 DocumentBuilderFactory 的实例。这里的实例的真正的类是继承自 javax.xml.parsers.DocumentBuilderFactory,由 SPI 的实现所提供的。如在 Apache Xerces 中,实现的类是 org.apache.xerces.jaxp.DocumentBuilderFactoryImpl。而问题在于,SPI 的接口是 Java 核心库的一部分,是由引导类加载器来加载的;SPI 实现的 Java 类一般是由系统类加载器来加载的。引导类加载器是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说,类加载器的代理模式无法解决这个问题。
线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。
java语言是一种先天具备动态性的语言,之所以具备动态性正是由于java类加载器的存在,这给我们的开发带来革命性的变革,因为我们可以开发出有弹性,已扩展的程序,从而大大提高我们程序的可维护性。
JVM通过类加载器加载类有2中方式:
预先载入和按需载入
通常基础类库需要预先载入(常驻内存),自定义类库按需载入(使用时载入,使用完毕由垃圾回收器负责释放);
演示代码:
public class TestLoadClass{ public static void main(String[] args){ B b = new B(); b.showMe(); } } class A{ public void showMe(){ System.out.println("I'm class A"); } } class B extends A{ public void showMe(){ System.out.println("I'm class B"); } }
执行结果:
I'm class B D:\javawork>java -verbose:class TestLoadClass [Loaded java.lang.Object from shared objects file] [Loaded java.io.Serializable from shared objects file] [Loaded java.lang.Comparable from shared objects file] [Loaded java.lang.CharSequence from shared objects file] [Loaded java.lang.String from shared objects file] [Loaded java.lang.reflect.GenericDeclaration from shared objects file] ............ 省略 ............ [Loaded TestLoadClass from file:/D:/javawork/] [Loaded java.lang.Void from shared objects file] [Loaded A from file:/D:/javawork/] [Loaded B from file:/D:/javawork/] I'm class B
从上面的结果可以看出,基础类库首先加载,然后加载主类,在执行的过程中需要Class B 时,首先加载了B的父类,然后加载B.
按需加载也可以分为2中:
1:使用new关键字创建对象时,如Dog dog = new Dog();
2:使用Class.forName(String name);
有两个forName()方法,一个是只有一个参数的(就是之前程序之中所使用的):
public static Class forName(String className);
另外一个是需要三个参数的:
public static Class forName(String name, boolean initialize,ClassLoader loader)
这两个方法,最后都是连接到原生方法
forName0(),
如下:
private static native Class forName0(String name,boolean initialize, ClassLoader loader) throws ClassNotFoundException;
只有一个参数的forName()方法,最后调用的是:
forName0(className, true,ClassLoader.getCallerClassLoader());
而具有三个参数的forName()方法,最后调用的是:
forName0(name, initialize, loader);
其中true表示是否初始化该class,loader表示用那个类加载器加载该class,默认使用调用该class的的类的类加载器:如,在A类中有,Class.forName(B);在使用加载A类的类加载器加载B类。
自定义类加载器:
有些时候系统提供的类加载器并不能满足我们的要,如需要加载的类是加密过的。此时就需要我们使用自定义的类加载器来加载指定的类。
基本上所有的类加载器都是 java.lang.ClassLoader 类的一个实例。下面详细介绍这个 Java 类。
java.lang.ClassLoader 类介绍
java.lang.ClassLoader 类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即 java.lang.Class 类的一个实例。除此之外,ClassLoader 还负责加载 Java 应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader 提供了一系列的方法,比较重要的方法如 表 1 所示。关于这些方法的细节会在下面进行介绍。
表 1. ClassLoader 中与加载类相关的方法
方法 说明
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name 的类,返回的结果是 java.lang.Class 类的实例。该方法在前面已经介绍了,如果想改变默认的委托代理模式可以覆盖该方法的实现。
findClass(String name) 查找名称为 name 的类,返回的结果是 java.lang.Class 类的实例,在该类中的实现是:
protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
从代码可以看出我们实现自定义类加载器可以覆盖该方法实现我们自己的加载逻辑,该方法在loadClass()中被调用。
findLoadedClass(String name) 查找名称为 name 的已经被加载过的类,返回的结果是 java.lang.Class 类的实例。该方法会调用一个native方法: private native final Class findLoadedClass0(String name);
defineClass(String name, byte[] b, int off, int len) 把字节数组 b 中的内容转换成 Java 类,返回的结果是 java.lang.Class 类的实例。这个方法被声明为 final 的。
resolveClass(Class<?> c) 链接指定的 Java 类。
对于 表 1 中给出的方法,表示类名称的 name 参数的值是类的二进制名称。需要注意的是内部类的表示,如 com.example.Sample$1 和 com.example.Sample$Inner 等表示方式。
文件系统类加载器
第一个类加载器用来加载存储在文件系统上的 Java 字节代码。完整的实现如下所示。
public class FileSystemClassLoader extends ClassLoader { private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; } protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] classData = getClassData(name); if (classData == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classData, 0, classData.length); } } private byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream ins = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead = 0; while ((bytesNumRead = ins.read(buffer)) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } private String classNameToPath(String className) { return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class"; } }
如上代码所示,类 FileSystemClassLoader 继承自类 java.lang.ClassLoader。在 表 1 中列出的 java.lang.ClassLoader 类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name) 方法即可。java.lang.ClassLoader 类的方法 loadClass() 封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass() 方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass() 方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass() 方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass() 方法,而是覆写 findClass() 方法。
类 FileSystemClassLoader 的 findClass() 方法首先根据类的全名在硬盘上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
在介绍完如何开发自己的类加载器之后,下面说明类加载器和 Web 容器的关系。
类加载器与 Web 容器
对于运行在 Java EE 容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用(在tomcat中用一个Context表示)都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
绝大多数情况下,Web 应用的开发人员不需要考虑与类加载器相关的细节。下面给出几条简单的原则:
每个 Web 应用自己的 Java 类文件和使用的库的 jar 包,分别放在 WEB-INF/classes 和 WEB-INF/lib 目录下面。
多个应用共享的 Java 类文件和 jar 包,分别放在 Web 容器指定的由所有 Web 应用共享的目录下面。
当出现找不到类的错误时,检查当前类的类加载器和当前线程的上下文类加载器是否正确。
总结
类加载器是 Java 语言的一个创新。它使得动态安装和更新软件组件成为可能。本文详细介绍了类加载器的相关话题,包括基本概念、代理模式、线程上下文类加载器、与 Web 容器关系等。开发人员在遇到 ClassNotFoundException 和 NoClassDefFoundError 等异常的时候,应该检查抛出异常的类的类加载器和当前线程的上下文类加载器,从中可以发现问题的所在。在开发自己的类加载器的时候,需要注意与已有的类加载器组织结构的协调。