Java ClassLoader机制

本文深入解析Java类加载器的工作原理,详细阐述了bootstrap、extension和system类加载器之间的关系,以及如何在运行时动态载入和更新类。重点介绍了如何利用多形性原理,通过类加载器的层次结构实现类的动态载入,并实例演示了如何规避类加载器的限制,灵活地在不同上下文中加载和使用类。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


======================================================
注:本文源代码点此下载
======================================================

当jvm(java虚拟机)启动时,会形成由三个类加载器组成的初始类加载器层次结构:

bootstrap classloader

|

extension classloader

|

system classloader

bootstrap classloader -引导(也称为原始)类加载器,它负责加载java的核心类。在sun的jvm中,在执行java的命令中使用-xbootclasspath选项或使用 - d选项指定sun.boot.class.path系统属性值可以指定附加的类。这个加载器的是非常特殊的,它实际上不是 java.lang.classloader的子类,而是由jvm自身实现的。大家可以通过执行以下代码来获得bootstrap classloader加载了那些核心类库:

url[] urls=sun.misc.launcher.getbootstrapclasspath().geturls();

for (int i = 0; i java代码

查看源代码

打印

01.public class launcher{

02.public launcher() {

03.extclassloader extclassloader;

04.try {

05.//初始化extension classloader

06.extclassloader = extclassloader.getextclassloader();

07.} catch(ioexception ioexception) {

08.throw new internalerror("could not create extension class loader");

09.}

10.try {

11.//初始化system classloader,parent是extension classloader

12.loader = appclassloader.getappclassloader(extclassloader);

13.} catch(ioexception ioexception1) {

14.throw new internalerror("could not create application class loader");

15.}

16.//将system classloader设置成当前线程的context classloader(将在后面加以介绍)

17.thread.currentthread().setcontextclassloader(loader);

18.......

19.}

20.public classloader getclassloader() {

21.//返回system classloader

22.return loader;

23.}

24.}

25.

26.extension classloader的部分代码:

27.static class launcher$extclassloader extends urlclassloader {

28.

29.public static launcher$extclassloader getextclassloader()

30.throws ioexception

31.{

32.file afile[] = getextdirs();

33.return (launcher$extclassloader)accesscontroller.doprivileged(new launcher$1(afile));

34.}

35.private static file[] getextdirs() {

36.//获得系统属性“java.ext.dirs”

37.string s = system.getproperty("java.ext.dirs");

38.file afile[];

39.if(s != null) {

40.stringtokenizer stringtokenizer = new stringtokenizer(s, file.pathseparator);

41.int i = stringtokenizer.counttokens();

42.afile = new file;

43.for(int j = 0; j

44.afile[j] = new file(stringtokenizer.nexttoken());

45.

46.} else {

47.afile = new file[0];

48.}

49.return afile;

50.}

51.}

52.

53.system classloader的部分代码:

54.static class launcher$appclassloader extends urlclassloader

55.{

56.

57.public static classloader getappclassloader(classloader classloader)

58.throws ioexception

59.{

60.//获得系统属性“java.class.path”

61.string s = system.getproperty("java.class.path");

62.file afile[] = s != null ? launcher.access$200(s) : new file[0];

63.return (launcher$appclassloader)accesscontroller.doprivileged(new launcher$2(s, afile, classloader));

64.}

65.}

看了源代码大家就清楚了吧,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就是用这种机制加载的.

好了,现在我们了解了classloader的结构和工作原理,那么我们如何实现在运行时的动态载入和更新呢?只要我们能够动态改变类搜索路径和清除classloader的cache中已经载入的class就行了,有两个方案,一是我们继承一个classloader,覆盖loadclass方法,动态的寻找class文件并使用defineclass方法来;另一个则非常简单实用,只要重新使用一个新的类搜索路径来new一个classloader就行了,这样即更新了类搜索路径以便来载入新的class,也重新生成了一个空白的cache(当然,类搜索路径不一定必须更改)。噢,太好了,我们几乎不用做什么工作,java.neturlclassloader正是一个符合我们要求的classloader!我们可以直接使用或者继承它就可以了!

这是j2se1.4 api的doc中urlclassloader的两个构造器的描述:

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.

其中url[] urls就是我们要设置的类搜索路径,parent就是这个classloader的parent classloader,默认的是system classloader。

好,现在我们能够动态的载入class了,这样我们就可以利用newinstance方法来获得一个object。但我们如何将此object造型呢?可以将此object造型成它本身的class吗?

首先让我们来分析一下java源文件的编译,运行吧!javac命令是调用“java_home/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:\ 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使用launcher初始化的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能够定位和载入这个class从classpath中),载入的也不是classpath中的这个class,而是从 classpath外动态载入的,这样总行了吧!十分不幸的是,这时会出现“java.lang.classcastexception”违例。

为什么呢?我们也来分析一下,不错,我们虽然从classpath外使用我们自己的classloader动态载入了这个class,但将它的实例造型的时候是jvm会使用system classloader来再次载入这个class,并尝试将使用我们的自己的classloader载入的class的一个实例造型为system 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就行了。

这样我们就可以事先定义好一组接口或者基类并放入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:文件:/c:/j2sdk1.4.1_01/jre/lib/rt.jar!/java/lang/string.class

表明string.class文件在rt.jar的java/lang目录中。

因此我们可以将图片等资源随同class一同打包到jar类库中(当然,也可单独打包这些资源)并添加它们到class loader的搜索路径中,我们就可以无需关心这些资源的具体位置,让class loader来帮我们寻找了!

本文由papa于2009-06-02 00:18:33.0在http://www.faceye.com发布

看了源代码大家就清楚了吧,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就是用这种机制加载的.

好了,现在我们了解了classloader的结构和工作原理,那么我们如何实现在运行时的动态载入和更新呢?只要我们能够动态改变类搜索路径和清除classloader的cache中已经载入的class就行了,有两个方案,一是我们继承一个classloader,覆盖loadclass方法,动态的寻找class文件并使用defineclass方法来;另一个则非常简单实用,只要重新使用一个新的类搜索路径来new一个classloader就行了,这样即更新了类搜索路径以便来载入新的class,也重新生成了一个空白的cache(当然,类搜索路径不一定必须更改)。噢,太好了,我们几乎不用做什么工作,java.neturlclassloader正是一个符合我们要求的classloader!我们可以直接使用或者继承它就可以了!

这是j2se1.4 api的doc中urlclassloader的两个构造器的描述:

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.

其中url[] urls就是我们要设置的类搜索路径,parent就是这个classloader的parent classloader,默认的是system classloader。

好,现在我们能够动态的载入class了,这样我们就可以利用newinstance方法来获得一个object。但我们如何将此object造型呢?可以将此object造型成它本身的class吗?

首先让我们来分析一下java源文件的编译,运行吧!javac命令是调用“java_home/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:\ 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使用launcher初始化的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能够定位和载入这个class从classpath中),载入的也不是classpath中的这个class,而是从 classpath外动态载入的,这样总行了吧!十分不幸的是,这时会出现“java.lang.classcastexception”违例。

为什么呢?我们也来分析一下,不错,我们虽然从classpath外使用我们自己的classloader动态载入了这个class,但将它的实例造型的时候是jvm会使用system classloader来再次载入这个class,并尝试将使用我们的自己的classloader载入的class的一个实例造型为system 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就行了。

这样我们就可以事先定义好一组接口或者基类并放入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:文件:/c:/j2sdk1.4.1_01/jre/lib/rt.jar!/java/lang/string.class

表明string.class文件在rt.jar的java/lang目录中。

因此我们可以将图片等资源随同class一同打包到jar类库中(当然,也可单独打包这些资源)并添加它们到class loader的搜索路径中,我们就可以无需关心这些资源的具体位置,让class loader来帮我们寻找了!


======================================================
在最后,我邀请大家参加新浪APP,就是新浪免费送大家的一个空间,支持PHP+MySql,免费二级域名,免费域名绑定 这个是我邀请的地址,您通过这个链接注册即为我的好友,并获赠云豆500个,价值5元哦!短网址是http://t.cn/SXOiLh我创建的小站每天访客已经达到2000+了,每天挂广告赚50+元哦,呵呵,饭钱不愁了,\(^o^)/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值