Java ClassLoader深入讲解
【转】http://www.blogjava.net/lhulcn618/archive/2006/05/25/48230.html
【参考】http://ifeve.com/classloader/
当JVM(Java虚拟机)启动时,会形成三个类加载器组成的初始类加载器层次结构。
bootstrap classloader
|
extension classloader
|
system classloader
①,当启动一个JVM时,bootstrap classloader会加载java的核心类,例如:rt.jar中的类。bootstrap classloader是其他classloader的parent,它是唯一一个没有parent的classloader
②,接下来是extension classloader,它以bootstrap classloader作为parent,它用来从Java系统变量java.ext.dir中的jar包中加载类的;
③,最后,也是最重要的一个就是开发者使用的system classpath classloader。它是extension classloader的child,它用来从Java系统变量java.class.path下面加载类,可以通过-classpath来制定这个位置。
注意类加载器的体系并不是“继承”体系,而是一个“委派”体系。大多数类加载器首先会到自己的parent中查找类或者资源,如果找不到,才会在自己的本地进行查找。事实上,类加载器被定义加载哪些在parent中无法加载到的类,这样在较高层级的类加载器上的类型能够被“赋值”为较低类加载器加载的类型。
类加载器的委托行为动机是为了避免相同的类被加载多次。(回到1995年,Java的主要方向被放在Applet上,那时候网络带宽优先,所以程序中的类直到用时才会被加载。但是事实上,Java在服务器端展示 了强劲的能力,但是服务器端要求类加载器能够反转委派原则,也就是先加载本地的类,如果加载不到,再到parent中加载。)
JavaEE 的委派模型:
每个方块都是一个classloader,JavaEE规范推荐每个模块的classloader先加载自己classloader的内容,如果加载不到才会到parent的classloader中尝试加载。
反转委派原则的原因是应用服务器中所携带的类库并不是应用所希望的,也许不适合应用开发者,一个常见的例子就是log4j的以来在容器和不同的应用中都存在,但是他们的版本大都不同。
Tomcat的类加载顺序(开启了delegate模式)
在Tomcat中,莫仍的行为是先尝试在bootstrap classloader和extension classloader中进行类型加载,如果加载不到则在WebappLoader中进行加载,如果还是找不到则在Common中进行查找。在Alibaba使用的Tomcat开启了delegate模式,因此加载类型时是会以parent classloader优先。
1、bootstrap classloader
bootstrap classloader-引导(也称为原始)类加载器,它负责加载Java的核心类库。在Sun的JVM中,在执行java的命令中使用-Xbootclasspath选项或使用-D选项制定sun.boot.class.path系统属性值何以制定附加的类。这个加载器是非常特殊的,它实际上不是java.lang.ClassLoader的子类,而是由JVM自身实现的。大家可以通过执行以下代码来获得bootstrap classloader加载了哪些核心类库:
import java.net.URL;
public class Test {
public static void main(String[] args) {
URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
for(URL url : urls) {
System.out.println(url.toExternalForm());
}
}
}
结果为:
file:/C:/Program%20Files/Java/jre7/lib/resources.jar
file:/C:/Program%20Files/Java/jre7/lib/rt.jar
file:/C:/Program%20Files/Java/jre7/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jre7/lib/jsse.jar
file:/C:/Program%20Files/Java/jre7/lib/jce.jar
file:/C:/Program%20Files/Java/jre7/lib/charsets.jar
file:/C:/Program%20Files/Java/jre7/lib/jfr.jar
file:/C:/Program%20Files/Java/jre7/classes
这时大家知道了为什么我们不需要在系统属性CLASSPATH中指定这些类库了吧,因为JVM在启动的时候就自动加载他们了。
2、extension classloader
extension classloader - 扩展类加载器,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ex或者由java.ext.dirs系统属性指定的)中JAR的类包。这为引入除Java核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个JRE中启动的JVM都是通用的,所以放入这个目录的JAR类包对所有的JVM和system classloader都是可见的。在这个实例上调用方法getParent()总是返回空值null,因为引导加载器bootstrap classloader不是一个真正的ClassLoader实例。所以当大家执行以下代码时:
System.out.println(System.getProperty("java.ext.dirs"));
ClassLoader extensionClassLoader = ClassLoader.getSystemClassLoader().getParent();
System.out.println("the parent of extension classloader :" + extensionClassLoader.getParent());
结果为:
C:\Program Files\Java\jre7\lib\ext;C:\Windows\Sun\Java\lib\ext
the parent of extension classloader :null
extension classloader 是system classloader的parent,而bootstrap classloader是extension classloader的parent,但它不是一个实际的classloader,所以为null.
3、system classloader
system classloader - 系统(也称为应用)类加载器,它负责在JVM被启动时,加载来自在命令java中的-classpath或者java.class.path系统属性或者CLASSPATH操作系统属性所指定的JAR类包和类路径。
(.;%JAVA_HOME%\lib;%JAVA_HOME%\lib\tools.jar)
总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。如果没有特别指定,则用户自定义的任何类加载器都将该类加载器作为它的父加载器。执行以下代码即可获得:
System.out.println(System.getProperty("java.class.path"));
输出结果则为用户在系统属性里面设置的CLASSPATH。
4、ClassLoader的全盘负责机制和Cache机制
classloader加载类用的是全盘负责委托机制。所谓全盘负责,即是当一个classloader加载一个class的时候,这个Class所依赖的和引用的所有Class也由这个classloader负责载入(例如父类),除非是显示的使用另外一个classloader载入;委托机制则是先让parent(父)类加载器寻找(而不是super,它与parent classloader类不是继承关系),只有在parent找不到的时候才从自己的类路劲中去寻找。此外类加载还采用了cache机制,也就是如果cache中保存了这个Class就直接返回它,如果没有才从文件中读取和转换成Class,并存入cache,这就是为什么我们修改了Class但是必须重新启动JVM才能生效的原因。
每个ClassLoader加载Class的过程:
①,检测此Class是否载入过(即在cache中是否存在此Class),如果有到⑧,否则到②;
②,如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader),到④;
③,请求parent classloader载入,如果成功到⑧,否则到⑤;
④,请求JVM从bootstrap class中载入,如果成功到⑧;
⑤,寻找Class文件(从与此classloader相关的类路径中寻找),如果找不到则到⑦;
⑥,从文件中载入Class,到⑧;
⑦,抛出ClassNotFoundException;
⑧,返回Class;
其中⑤、⑥步我们可以通过覆盖ClassLoader的findClass方法来实现自己的载入策略。甚至覆盖loadClass方法来实现自己的载入过程。
5、类加载器的顺序:
先是bootstrap classloader,然后是extension classloader,最后才是system classloader。大家会发现加载的Class越重要的越在前面。这样做的原因是处于安全性的考虑,试想如果system classloader亲自加载一个具有破坏性的“java.lang.System”类的后果吧。这种委托机制保证了用户即使具有一个这样的类,也把它加入到了类路径中,但是它永远不会被载入,因为这个类总是由bootstrap classloader来加载的。大家可以执行一下以下的代码:
System.out.println(System.class.getClassLoader());
结果:null
结果是null,这就表明java.lang.System是由bootstrap classloader加载的,以为bootstrap classloader不是一个真正的ClassLoader实例,而是由JVM实现的,正如前面已经说过的、
类 Bootstrap
package com.sun.jdi;
import com.sun.tools.jdi.VirtualMachineManagerImpl;
public class Bootstrap {
public Bootstrap() {
}
public static synchronized VirtualMachineManager virtualMachineManager() {
return VirtualMachineManagerImpl.virtualMachineManager();
}
}
6、JVM建立类加载器的结构
下面就让我们来看看JVM是如何来为我们建立类加载器的结构的:
sum.misc.Launcher,顾名思义,当你执行java命令的时候,JVM会先使用bootstrap classloader载入并初始化一个Launcher,执行以下代码:
System.out.println("The Launcher`s classloader is " + sun.misc.Launcher.class.getClassLoader());
结果:The Launcher`s classloader is null
因为使用bootstrap classloader加载,所以classloader is null。
Launcher会根据系统和命令设定初始化好classloader结构,JVM就用它来获得extension classloader和system classloader并载入所有需要载入的Class,最后执行java命令指定的带有静态的main方法的Class。extension classloader实际上是sun.misc.Launcher$ExtClassLoader类的一个实例。system classloader实际上是sun.misc.Launcher$APPClassLoader类的一个实例。并且都是java.net.URLClassLoader的子类。
让我们来看看Laucher初始化的过程的部分代码:
public class Launcher {
public Launcher() {
ExtClassLoader localExtClassLoader;
try {
//初始化extension classloader
localExtClassLoader = ExtClassLoader.getExtClassLoader();
} catch (IOException localIOException1) {
throw new InternalError("Could not create extension class loader");
}
try {
//初始化system classloader,parent是extension classl
this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
} catch (IOException localIOException2) {
throw new InternalError("Could not create application class loader");
}
//将system classloader设置成当前线程的context classloader(将在后面介绍)
Thread.currentThread().setContextClassLoader(this.loader);
String str = System.getProperty("java.security.manager");
if (str != null) {
SecurityManager localSecurityManager = null;
if (("".equals(str)) || ("default".equals(str))) {
localSecurityManager = new SecurityManager();
} else {
try {
localSecurityManager = (SecurityManager) this.loader
.loadClass(str).newInstance();
} catch (IllegalAccessException localIllegalAccessException) {
} catch (InstantiationException localInstantiationException) {
} catch (ClassNotFoundException localClassNotFoundException) {
} catch (ClassCastException localClassCastException) {
}
}
if (localSecurityManager != null) {
System.setSecurityManager(localSecurityManager);
} else {
throw new InternalError("Could not create SecurityManager: "
+ str);
}
}
}
......
//返回system classloader
public ClassLoader getClassLoader() {
return this.loader;
}
}
再看一下extension classloader的部分代码:(Launcher$ExtClassLoader内部类)
static class ExtClassLoader extends URLClassLoader {
public static ExtClassLoader getExtClassLoader() throws IOException {
File[] arrayOfFile = getExtDirs();
try {
(ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Launcher.ExtClassLoader run() throws IOException {
int i = this.val$dirs.length;
for (int j = 0; j < i; j++) {
MetaIndex.registerDirectory(this.val$dirs[j]);
}
return new Launcher.ExtClassLoader(this.val$dirs);
}
});
} catch (PrivilegedActionException localPrivilegedActionException) {
throw ((IOException)localPrivilegedActionException.getException());
}
}
void addExtURL(URL paramURL) {
super.addURL(paramURL);
}
public ExtClassLoader(File[] paramArrayOfFile) throws IOException {
super(null, Launcher.factory);
}
private static File[] getExtDirs() {
//获得系统属性java.ext.dirs
String str = System.getProperty("java.ext.dirs");
File[] arrayOfFile;
if (str != null) {
StringTokenizer localStringTokenizer = new StringTokenizer(str, File.pathSeparator);
int i = localStringTokenizer.countTokens();
arrayOfFile = new File[i];
for (int j = 0; j < i; j++) {
arrayOfFile[j] = new File(localStringTokenizer.nextToken());
}
} else {
arrayOfFile = new File[0];
}
return arrayOfFile;
}
...
}
再看system classloader的部分代码:(Launcher$AppClassLoader内部类)
static class AppClassLoader extends URLClassLoader {
public static ClassLoader getAppClassLoader(final ClassLoader paramClassLoader) throws IOException {
//获得系统属性“java.class.path”
String str = System.getProperty("java.class.path");
final File[] arrayOfFile = str == null ? new File[0] : Launcher.getClassPath(str);
(ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
public Launcher.AppClassLoader run() {
URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.pathToURLs(arrayOfFile);
return new Launcher.AppClassLoader(arrayOfURL, paramClassLoader);
}
});
}
}
看了源代码大家就清楚了吧,extension classloader是使用系统属性“java.ext.dirs”设置类搜索路径的,并没有使用parent。system classloader是使用系统属性“java.class.path”设置类搜索路径的,并且有一个parent classloader。Launcher初始化extension classloader,system classloader,并将system classloader设置成为context classloader,但是仅仅返回system classloader给JVM。
这里怎么又出来一个context classloader呢?它有什么用?我们在建立一个线程Thread的时候,可以为这个线程通过setContextClassLoader方法来制定一个合适的classloader作为这个线程测context classloader,当此线程运行的时候,我们可以通过getContextClassLoader方法来获得此context classloader,就可以用它来载入我们所需要的Class,默认的是system classloader。利用这个特性,我们可以打破classloader的委托机制了。父classloader可以获得当前线程的context classloader,而这个context classloader可以使它的子classloader或者其他的classloader,那么父classloader就可以从其获得所需的Class,这就打破了只能向父classloader请求的限制了。这个机制可以满足当我们的classpath是在运行时才确定,并由定制的classloader加载的时候,由system classloader(即在jvm classpath中)加载的class可以通过context classloader获得定制的classloader并载入特定的class(通常是抽象类和接口,定制的classloader中是其实现),例如web应用中的servlet就是这种机制加载的。
7、动态载入和更新
好了,现在我们了解了classloader的结构和工作原理,那么我们如何实现在运行时的动态载入和更新呢?只要我们能够动态改变类搜索路径和清除classloader的cache中已经载入的Class就行了,有两个方案。一是我们继承一个classloader,覆盖loadclass方法,动态的寻找Class文件并使用defineClass方法;另一个则非常简单实用,只要重新使用一个新的类搜索路劲来new一个classloader就行了,这样即更新了类搜索路径以便来载入新的Class,也重新生成了一个空白的cache(当然,类搜索路径不一定必须更改)。我们几乎不用做什么工作,java.netURLClassLoader正是一个符合我们要求的classloader,我们可以直接使用或者继承它就可以了!
Constructor and Description:
URLClassLoader(URL[] urls)
Constructs a new URLClassLoader for the specified URLs using the default delegation parent ClassLoader.
URLClassLoader(URL[] urls, ClassLoader parent)
Constructs a new URLClassLoader for the given URLs.
URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory)
Constructs a new URLClassLoader for the specified URLs, parent class loader, and URLStreamHandlerFactory.
其中URL[] urls就是我们要设置的类搜索路径,parent就是这个classloader的parent classloader,默认的是system classloader。
现在我们能够动态的载入Class了,这样我们就可以利用newInstance来获得一个Object。但我们如果将此Object造型呢?可以将此Object造型成它本省的Class吗?
首先让我们来分析一下java源文件的编译,运行吧!javac命令是调用“JAVA_HOM/lib/tools.jar”中的“com.sun.tools.javac.Main”的compile方法来编译:
public static int compile(String as[]);
public static int compile(String as[], PrintWriter printwriter);
返回0表示编译成功,字符串数组as则是我们使用javac命令编译时的参数,以空格划分。例如:
javac -classpath c:\foo\bar.jar;. -d c:\Some.java
则字符串数组as为{"-classpath","c:\\foo\\bar.jar;.","-d","c:\\","c:\\Some.java"},如果带有PrintWriter参数,则会把编译信息输出到这个指定的printwriter中。默认的输出是System.err。
其中Main是由JVM使用Launcher初始化system classloader载入的,根据全盘负责原则,编译器在解析这个java源文件时所发现的它所依赖和引用的所有Class也将由system classloader载入,如果system classloader不能载入某个Class,编译器将抛出一个“cannot resolve symbol”错误。
所以首先变异就不会通过,也就是编译器无法编译一个引用了在CLASSPATH中位置Class的java源文件,而由于拼写错误或者没有吧所需类库放到CLASSPATH中,大家一定经常看到这个“cannot resolve symbol”这个编译错误吧!
其次,就是我们把这个Class放到编译路径中,成功的进行了编译,然后在运行的时候不把它放到CLASSPATH中而利用我们自己的classloader来动态载入这个Class,这时候也会出现“java.lang.NoClassDefFoundError”的违例,为什么呢?
我们再来分析一下,首先调用这个造型语句的可执行的Class一定是由JVM使用Laucher初始化的system classloader载入的,根据全盘负责原则,但我们进行造型的时候,JVM也会使用system classloader来蚕食载入这个Class来对实例进行造型,自然在system classloader寻找不到这个Class时就会抛出“java.lang.NoClassDefFoundError”的违例。
OK,现在我们总结一下,java文件的变异和Class的载入执行,都是适应Launcher初始化system classloader作为类载入器的,我们无法动态的改变system classloader,更无法让JVM使用我们自己的classloader来替换system classloader,根据全盘负责原则,就限制了编译和运行时,我们无法直接显示的使用system classloader寻找不到的Class,即我们只能使用Java核心类库,扩展类库和CLASSPATH中的类库中的Class。
还不死心!再尝试一下这种情况:我们把这个Class也放到CLASSPATH中,让system classloader能够识别和载入。然后我们通过自己的classloader来从指定的class文件中载入这个Class(不能够委托parent载入,因为这样会被system classloader从CLASSPATH中将其载入)。然后实例化一个Object,并造型成这个Class,这样JVM也是别这个Class(因为system classloader能够从CLASSPATH中定位和载入这个Class),载入的也不是CLASSPATH中的这个Class,而是从CLASSPATH外动态载入的,这样总行了吧,十分不幸的是,这时会出现“java.lang.ClassCastException”违例。
为什么呢?我们也来分析一下,不错,我们虽然从CLASSPATH外使用我们自己的classload动态载入了这个Class,但将它的实例造型的时候是JVM使用system classloader来在此载入这个Class,而我们使用的是自己classloader并载入的这个Class的一个实力造型。大家发现什么问题了吗?也就是我们尝试将从一个classloader载入的Class的一个实例造型是另外一个classloader载入的Class,虽然两个Class的名字一样,甚至是从同一个class文件中载入。但不幸的是JVM却认为这两个Class是不同的,即JVM认为不同的classloader载入的相同的名字的Class是不同的(即使是从同一个class文件中载入的)!这样做的原因我想大概也是主要出于安全性考虑,这样就保证所有的和兴Java类都是system classloader载入的,我们无法用自己的classloader载入相同名字的Class的实例来替换他们的实例。
到这里,应该就想到了该如何动态载入我们的Class,实例化,造型并调用了。
那就是利用面向对象的基本特性之一的多态性。我们把我们动态载入的Class的实例造型成它的一个system classloader所能识别的父类就行了,这是为什么呢,我们还是要再分析一下。当我们用我们自己的classloader来动态载入这个Class的时候,发现它有一个父类Class,在载入它之前JVM必须先载入这个父类的Class,这个父类Class是system classloader所能识别的,根据委托机制,它将由system classloader载入,然后我们的classloader再载入这个子类Class,创建一个实例,造型为这个父类Class,注意了,造型成这个父类Class的时候(向上转型)是面向对象的java语言所允许的并且JVM也是支持的,JVM就是用system classloader再次载入这个父类Class,然后将此子类实例造型为这个父类的Class。大家可以从这个过程中发现这个父类Class都是有system classloader载入的,也就是同一个class loader载入的同一个Class,所以造型的时候不会出现任何异常。而根据多态性,调用这个父类的方法时,真正执行的是这个子类Class(非父类Class)的覆盖了父类的方法。这些方法中也可以引用system classloader不能识别的Class,因为根据全盘负责原则,只要载入这个Class的classloader即我们自己定义的classloader能够定位和载入这些Class就行了。(即牛人说的Java文件在生成class文件的编译过程中,代码就已经写好了的)
这样我们就可以事先定义好一组接口或者基类并放入CLASSPATH中,然后在执行的时候动态的载入实现或者继承了这些接口或基类的子类。比如Servlet,web application server能够载入任何继承了Servlet的Class并正确的执行它们,不管它实际的Class是什么,就是都把它们实例化称为一个Servlet Class,然后执行Servlet init doPost doGet destroy..等方法的,而不管这个Servlet是从web-inf/lib和web-inf/classes下由system classloader的子classloader(即定制的classloader)动态载入的。说了这么多希望大家都明白了。再applet,ejb容器中,都是采用了这种机制。
对于以上各种情况,希望大家实际编写一些example来实验一下。
最后说点别的,classloader虽然称为类加载器,但并不意味着只能用来加载Class,我们还可以利用它来获得图片,音频文件等资源的URL,当然,这些资源必须在CLASSPATH中的jar类库或者目录下。在API的doc中关于ClassLoader的两个寻找资源和Class的方法描述吧:
public URL getResource(String name)
用指定的名字来查找资源,一个资源是一些能够被class代码访问的在某种程度上依赖于代码位置的数据(图片,音频,文本等等)。一个资源的名字是以'/'号分隔确定资源的路径名的。这个方法将先请求parent classloader搜索资源,如果没有parent,则会在内置在虚拟机中的classloader(即bootstrap classloader)的路径中搜索。如果失败,这个方法将调用findResource(String)来寻找资源。
public static URL getSystemResource(String name)
从用来载入类的搜索路径中查找一个指定名字的资源。这个方法使用system class loader来定位资源。即相当于ClassLoader.getSystemClassLoader().getResource(name)。
例如:
System.out.println(ClassLoader.getSystemResource("java/lang/String.class"));
结果:jar:file:/C:/Program%20Files/Java/jre7/lib/rt.jar!/java/lang/String.class
表明String.class文件在rt。jar的java/lang目录中。
因此我们可以将图片等资源随同Class一同打包到jar类库中(当然,也可以单独打包这些资源),并添加他们到class loader的搜索路径中,我们就可以无需关心这些资源的具体位置,让class loader来帮我们寻找了。

浙公网安备 33010602011771号