java类加载器

java类从加载到虚拟机内存开始,到卸载出内存为止,会经历7个阶段:加载、验证、准备、解析、初始化、使用、卸载。其中验证、准备、解析3个部分统称为链接。 类加载器用于实现类的加载操作,通过类加载器可以让应用程序自己决定获取所需类的方式。
但类加载器的作用不止如此,在java虚拟机中,类和接口不仅仅是由它的名称来确定,而是由一个值对:类的全限定名和类的定义类加载器所共同确定。这里涉及到类加载器的分类。
类加载器L可以通过直接定义或者委托的方式来加载类C,如果L直接创建了类C,那么L是C的定义加载器(defining loader)
如果一个类加载器把加载请求委托给其他类加载器,发出加载请求的加载器和最终完成加载和定义类的类加载器并不一定是同一个加载器。发出加载请求的类加载器称为初始加载器(initiating loader)。一个类的定义加载器是它所引用的其他类的初始加载器。

类加载器的分类

类加载器分为3类,启动类加载器、扩展类加载器、系统类加载器(又称 应用程序类加载器),当然我们也可以自定义类加载器,算上这种的话就是4类了。启动类加载器一般使用其他语言实现,是虚拟机自身的一部分,其他两种类加载器都是由java语言实现,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader。但更细致地说,它们继承于非抽象类URLClassLoader,这个类是ClassLoader的一个实现类。可以通过这个类来加载jar文件或者目录之中的class文件。
URLClassLoader封装了类加载器一些操作的具体实现,所以我们在自定义类加载器的时候,如果情况合适,建议继承URLClassLoader,这样比直接继承抽象类ClassLoader少些一些逻辑代码。

启动类加载器(Bootstrap ClassLoader):这个加载器负责将存放在< JAVA_HOME >\lib(jre所在目录\lib)目录中,或者被-Xbootclasspath参数所指定的路径中的,并且被虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录也不会被加载)类库加载到虚拟机内存中。
启动类加载器无法被程序直接引用,如果需要把请求委托给启动类加载器,可以直接使用null代替。

Xbootclasspath参数的使用方法:

Xbootclasspath:完全取代系统Java classpath.最好不用。
-Xbootclasspath/a: 在系统class加载后加载。一般用这个。
-Xbootclasspath/p: 在系统class加载前加载,注意使用,和系统类冲突就不好了.
win32 java -Xbootclasspath/a: some.jar;some2.jar; -jar test.jar
unix java -Xbootclasspath/a: some.jar:some2.jar: -jar test.jar
win32系统每个jar用分号隔开,unix系统下用冒号隔开

扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载< JAVA_HOME >\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用程序类加载器(Application ClassLoader):这个加载器由sun.misc.Launcher$AppClassLoader实现,由于这个类加载器是ClassLoader中getSystemClassLoader()的返回值,所以也称系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库。开发者可以直接使用这个类加载器,如果应用程序没有自定义类加载器,一般情况下这就是程序的默认类加载器。
用户类路径就是workspace/对应程序/bin目录。

类加载器的搜索目录:
每个类加载器的搜寻范围在它的描述处已经说明,如果想通过代码查看上述类加载器的搜索目录,可以通过以下方法:
对于启动类加载器,可以通过System.getProperty("sun.boot.class.path")获取;
对于扩展类加载器和应用程序类加载器,由于它们都是URLClassLoader的实现类,可以通过URLClassLoader的getURLS()获取搜索目录。扩展类加载器也可通过System.getProperty("java.ext.dirs")来获取搜索目录。
代码如下:

    public static void main(String[] args) {
        // 应用程序类加载器
        URLClassLoader appcl = (URLClassLoader) ClassLoader.getSystemClassLoader();

        //扩展类加载器
        URLClassLoader extcl = (URLClassLoader)ClassLoader.getSystemClassLoader().getParent();

        System.out.println("启动类加载器搜索目录:\r\n\t" + System.getProperty("sun.boot.class.path").replaceAll(";",";\r\n\t"));

        System.out.print("扩展类加载器搜索目录:\r\n");
        for(URL url : extcl.getURLs()){
            System.out.print("\t");
            System.out.println(url);
        }

        System.out.print("应用程序类加载器搜索目录:\r\n");
        for(URL url : appcl.getURLs()){
            System.out.print("\t");
            System.out.println(url);
        }
    }

篇幅原因,结果我就不贴了,大家可以自己运行查看结果,这里也展示了如何获取系统类加载器和扩展类加载器的实例。

类加载器的双亲委派模型:

类加载器的层次结构:

类加载器的层次结构

图中展示的类加载器的层次关系就是类加载器的双亲委派模型,双亲委派模型要求除了启动类加载器外,其他类加载器都有父类加载器,这里类加载器的父子关系不是以继承的关系实现的,而是以组合的方式实现。

双亲委派模型的工作流程:如果一个类加载器收到一个类加载的请求,它首先不会自己加载,而是委托给父类加载器去完成,每一个层次的类加载器皆是如此。这样层层委托,这样加载请求最终都应该传送到顶层的启动类加载器,只有当父类加载器反馈自己无法完成加载要求时,子加载器才会去自己加载。

这里如何判断一个类加载器是否可以完成加载请求呢?这是根据类加载器能否在它的搜索范围内查找所需的类,而每个类加载器的范围在在讨论类加载的搜索目录时已经说明了。也就是说类加载器的职权范围由它的搜索目录确定

双亲委派模型的优势: 一个显而易见的优势就是Java类随着其类加载器一起具备了一种带有优先级的层次关系,例如java.lang.Object类,它存放在rt.jar中,所以无论哪个类加载器去加载它,委托请求都会到达启动类加载器,由启动类加载器去创建Object类,也就是说Object类的定义类加载器无论如何都是启动类加载器,也就保证了java虚拟机中Object类的唯一性(结合上面关于类在内存中的唯一标识的讨论),如果没有双亲委派模型,各个类加载器自行加载,那么会有很多不同的Object类,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;
        }
    }

注意:由于双亲委派模型的逻辑都在loadClass方法之中,所以在自定义类加载器时一般复写findClass方法而并非直接loadClass方法。

类加载器逻辑的连贯性保证

轻易的破坏loadClass方法的双亲委派模型的逻辑很危险,因为类加载过程中由于类的解析过程(详情可以搜索类加载过程的相关文章)需要通过它的类加载器去加载父类。所以类加载器也必须能递归完成父类的加载请求,不论是通过自己加载还是委托给父类加载器去加载。所以类的直接父类一直到Object类的加载请求类加载器中loadClass都必须要能够完成。否则就会出现错误。也就是说对于当前类以及当前类的父类和所引用的类,loadClass的类加载逻辑必须全部都可以处理。双亲委派模型符合这个条件,可以自行定义类加载逻辑,但必须满足这个条件。

定义类加载器和初始类加载器的易错点

我们知道,在java虚拟机的内部,类的唯一性由类名称和类的定义类加载器所确定,也就是说如果两个类C同名,但定义类加载器不同,这两个类可以在内存中共存,这两个类也是不同的,即类C和类C不是同一个类。所以C == C会返回false(前提是两个类的加载器不同)。

但我们要特别注意这里说的是定义类加载器。我在写代码的时候碰到过这样的情况,用一个自定义类加载器FileSystemClassLoader的两个实例去加载类A,根据上面的理论这两个类应该是不同的类,但判断结果却是两者相同,毫无疑问这里出现了问题。
问题的原因在于,在我的自定义类加载器中的loadClass方法使用了双亲委派模型的逻辑,而在系统类加载器的搜索范围,也就是应用程序的bin目录下有一个A.class。虽然我设置FileSystemClassLoader搜索路径是其他的路径D:\test,而且想要加载的类是D:\test\A.class而不是应用程序目录下的bin\A.class。但当FileSystemClassLoader把加载请求委托给系统类加载器时,它在自己的搜索范围内找到了名字为“A”的类(即bin\A.class文件中表示的类),并加以创建。所以所加载的两个类的类加载器的定义类加载器均为系统类加载器,所以它们是同一个类。这个问题很隐蔽。它反应了两个问题:

  • 要清楚类加载的委派流程,无论是双亲委派模型还是自定义的委派逻辑。明确类不是我们想让它由哪个类加载器加载就由哪个类加载器加载,而是要根据委派逻辑结合各个类加载器的搜索范围来决定。

  • 由于类加载器是根据类的名称来搜索和加载类的,所以要注意各级类加载器的搜索范围内是否有同名的类定义文件。

总结来说,由于类加载器单纯由类的名称来进行搜索从而加载类,而不是由那个特定的类定义文件来决定由哪个类加载器加载,所以要注意类定义文件是否是我们所想的那个类定义文件,类加载器是否是我们所想的那个类加载器。 关键在于明确类名称和类定义文件对类加载的作用,类名称决定用哪个类加载器,类定义文件决定从哪儿加载类定义

破坏类加载器的双亲委派模型:

双亲委派模型很好的解决了各个类加载器的基础类统一的问题。基础类之所以“基础”,是因为它们总是作为被用户代码调用的API,但世事无绝对,如果基础类需要回调用户的代码时,那该怎么办?一个典型的例子便是JNDI服务。它的代码由启动类加载器去加载(在JDK1.3时放进rt.jar)。 但JNDI的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的classpath下的JNDI接口提供者(SPI, Service Provider Interface)的代码,也就是所说的用户代码,但启动类加载器不可能”认识”之些代码,该怎么办?

我们先来讨论一下为什么说启动类加载器不可能“认识”这些代码? 比如在rt.jar中有一个类C引用了用户代码类A,C是位于rt.jar中由启动类加载器加载的类,A是位于用户程序路径中的类。在解析类A的引用时,之前我们讨论过,会用引用类C的类加载器 – 启动类加载器 去加载类A,但在启动类加载器的搜索范围能并无类A的class文件(位于用户程序路径中),而且也没有更高级的类加载器可以委托,所以无法加载类A,更不谈使用类A。所以说不可能“认识”这些代码。

接着讨论解决办法,可以在类C中使用其他的类加载器L来加载类A,但L必须能通过之前所说的连贯性保证。可以使用线程上下文类加载器,关于这个线程上下文加载器的详细可以参阅其他文章,线程上下文类加载器如果不做设置,默认的类加载器是系统类加载器,可以通过线程上下文加载器来获取系统类加载器进而加载A,还能满足连贯型保证。

URLClassLoader 与 ClassNotFoundException:

由于扩展类加载器和系统类加载器都是其实现,所以,这里所述注意情况对它们也使用。

创建URLClassLoader需要传入一个URL[]数组,这个URL[]便是URLClassLoader的搜索范围,
当需要加载的类处于某个包中时,要特别注意其对应的url。 我在使用URLClassLoader的时候遇到了一个问题。我有一个类A的定义文件位于D:\test\jvm\BB.class。

package jvm;

public class BB {

}

这个BB类声明位于jvm包中,通过路径“D:\test\jvm\”来取得url,然后在loadClass(“jvm.BB”)时报ClassNotFoundException,后来把路径改为“D:\test\”便可以了。 原因在于,loadClass()在解析”jvm.BB”时会把之前包名jvm一同解析,且在路径后面加上”.class”后缀,导致整体的路径变成“D:\test\jvm\jvm\BB.class”。这个路径自然找不到。
由于loadClass()(准确是findClass方法)在解析时会加上包名的路径,所以:
一方面url中要注意不包含包名的路径,;
另一方面是,在实际的文件系统中类的定义要放在包名路径下,如com.sun.test包下的class A需要放在com/sun/test/A.class路径中。

对于jar文件是一样的道理,jar文件中类如果也处于包中,jar文件内(而不是外)也要有包名的路径。
总结:特别注意类的包名和路径的关系

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值