Java类加载器是用来在运行时加载类(*.class文件)。Java类加载器基于三个原则:委托、可见性、唯一性。委托原则把加载类的请求转发给父 类加载器,而且仅加载类当父 类加载器无法找到或者不能加载类时。可见性原则允许子类加载器查看由父类加载器加载的所有的类,但是父类加载器不能查看由子类加载器加载的类。唯一性原则只允许加载一次类文件,这基本上是通过委托原则来实现的并确保子类加载器不重新加载由父类加载器加载过的类。正确的理解类加载器原理必须解决像 NoClassDefFoundError in Java、 java.lang.ClassNotFoundException(这些现象和加载类有关系)的问题。类加载器在高级Java面试中也是一个重要的课题,在此场合中对类加载器和类路径的工作原理有一定的了解是Java程序员所预期的。我在各种Java面试中经常遇见这样的问题:在Java中一个类可以被两个不同的类加载器加载吗?在本文中,我们将学习什么是Java中的类加载器、它的工作原理以及与它有关的一些细节。
Java的类加载器是什么
Java中的类加载器是加载Java类文件(*.class)的一个类。Java代码被javac 编译器编译后以字节码的形式保存到类文件,JVM(Java虚拟机)通过操作类文件里的字节码来执行Java程序。类加载器负责从文件系统、网络或任何其它资源中加载类文件。
Java中使用的默认类加载器有以下三种:Bootstrap , Extension以及System or Application class loader。每个类加载器都有一个预定义的位置,它们在那里加载类文件。Bootstrap 类加载器负责从rt.jar中加载标准JDK类文件,并且它是Java中所有类加载器的父级。Bootstrap类加载器没有任何父级,如果你调用String.class.getClassLoader() 则返回null 而且与此相关的代码则抛出空指针异常。Bootstrap类加载器在java中也被称为Primordial ClassLoader(原生类加载器) 。Extension类加载器委托它的父级Bootstrap类加载器来加载类文件,如果委托失败,则从指向java.ext.dirs 系统属性的jre/lib/ext目录或者任何其它目录加载类文件。JVM中的Extension类加载器被sun.misc.Launcher$ExtClassLoader实现。
JVM 使用的第三种默认类加载器就是System or Applicationclass loader,它主要负责从CLASSPATH环境变量中加载应用程序特定的类文件,-classpath or -cp 命令行选项, JAR内部清单文件的类路径属性。Application类加载器是Extension类加载器的子级,它由sun.misc.Launcher$AppClassLoader类实现。而且,除Bootstrap 类加载器(大都由C 语言实现)以外,所有的Java类加载器使用java.lang.ClassLoader 来实现。
简而言之,这三种类加载器加载类文件的路径如下:
1) Bootstrap ClassLoader - JRE/lib/rt.jar
2) Extension ClassLoader - JRE/lib/ext 或者任何指向java.ext.dirs的路径
3) Application ClassLoader - CLASSPATH环境变量、-classpath or -cp 命令行选项,
JAR内部清单文件的类路径属性
正如我前面所描述的那样,java类加载器基于三个原则:委托、可见性、唯一性。在本节中我们将看到这些原则的细节之处以及通过示例来理解java类加载器工作原理。顺便说一句,这里有一个关系图,它解释了在java中通过使用委托类加载器如何加载类文件。
委托原则
在探讨java中的类文件在什么时候被加载和初始化的问题时,java中的类文件在它被需要的时候被加载。假设您有一个叫做Abc.class 的应用程序特定的类文件,加载这个类文件的第一个请求发送到 Application类加载器。Application类加载器委托它的父级Extension类加载器,而Extension类加载器委托给Bootstrap类加载器。Bootstrap类加载器在rt.jar 中寻找类文件,由于没有找到,请求转发到Extension类加载器。Extension类加载器在jre/lib/ext目录中寻找类文件并尝试加载类文件,如果找到了类文件,这时Extension类加载器将会加载类文件(Application类加载器将永远不会加载类文件);但是如果Extension类加载器没有加载类文件,那么Application类加载器将会从java类路径中加载类文件。请注意:类路径用于加载类文件,而路径用来查找可执行的像javac or java 的命令。
可见性原则
根据可见性原则,子级类加载器可以看到父级类加载器加载的类文件,但是反过来则行不通。这意味着,假如Application类加载器加载了Abc.class 文件,那么尝试使用Extension类加载器去加载Abc.class 文件,则会抛出异常java.lang.ClassNotFoundException。请看下面的示例:
packagetest;
importjava.util.logging.Level;
importjava.util.logging.Logger;
/**
* Java program to demonstrate How ClassLoader works in Java,
* in particular about visibility principle of ClassLoader.
*
* @author Javin Paul
*/
publicclass ClassLoaderTest {
publicstaticvoid main(String args[]){
try{
//printing ClassLoader of this class
System.out.println("ClassLoaderTest.getClass().getClassLoader() : "
+ ClassLoaderTest.class.getClassLoader());
//trying to explicitly load this class again using Extension class loader
Class.forName("test.ClassLoaderTest", true
, ClassLoaderTest.class.getClassLoader().getParent());
}catch(ClassNotFoundException ex){
Logger.getLogger(ClassLoaderTest.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
Output:
ClassLoaderTest.getClass().getClassLoader() : [email protected]
16/08/20122:43:48 AM test.ClassLoaderTest main
SEVERE: null
java.lang.ClassNotFoundException: test.ClassLoaderTest
at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at sun.misc.Launcher$ExtClassLoader.findClass(Launcher.java:229)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
at java.lang.Class.forName0(Native Method)
at java.lang.Class.forName(Class.java:247)
at test.ClassLoaderTest.main(ClassLoaderTest.java:29)
唯一性原则
根据这个原则,一个类文件被父级类加载器加载后,子级类加载器则不能加载它。尽管完全可以编写一个类加载器,它自己加载类文件,这违反了委托和唯一性原则,这样做没有什么好处。在您编写您自己的类加载器时,您应当遵守所有的类加载器原则。
java中如何准确加载类
java提供了API通过Class.forName(classname) 和Class.forName(classname, initialized, classloader)方法来明确加载一个类,还记得JDBC中用来加载JDBC驱动的方式吗,就是使用该机制来加载对应的类。就像我们在上边例子中展示的一样,你可以将加载特定类的加载器的名称连同类的二进制名称一起作为参数传进来。Class是通过调用java.lang.ClassLoader的loadClass()方法,该方法又会调用findClass()方法来为相应类查找对应的字节码。在这个例子中扩展类加载器使用java.lang.URLClassLoader用来在jar包和目录中查找类文件以及资源。其中,查询中所有以"/"结尾的的都会被当作目录处理。如果findClass()方法没有找到相关类,那么它会抛出java.lang.ClassNotFoundException异常;如果该方法找到了相关类,它会调用defineClass()方法来将字节码转换为.class的实例并将该实例返回给调用者。
java中在哪里使用类加载器
在Java中类加载器是一个强有力的概念,在许多地方都有使用。其中比较流行的一个例子就是applet中用来加载类的AppletClassLoader类加载器,由于applet是从互联网上加载而不是从本地文件系统中来加载类。通过使用单独的类加载器可以从多个不同的资源中加载多次相同的类,并且这些类在JVM中会被当作不同的类对待。J2EE使用不同的类加载器从不同的位置来加载类,比如war包中的类是通过Web-app类加载器来加载,而在EJB-JAR包中绑定的类则通过其他的类加载器加载。而一些web服务器也支持热部署功能,这都是通过实现ClassLoader来达到目的的。同时也可以使用ClassLoader来从数据库或者其他持久化存储系统中来加载类。
这就是所有关于java中的类加载器及其工作原理。我们已经了解了委托机制、可见性、唯一性原则,这些对代码调试或者解决java中类加载器相关问题时都很重要。总而言之,类加载器工作原理对每一位设计java应用和包的开发者和架构师所必备的知识。