译 java 中的类加载机制(1)

译 java 中的类加载机制(一)

Java类加载机制(一)

 

译:ayi  译文疏漏,请多多指点

原文:http://www.onjava.com/pub/a/onjava/2005/01/26/classloading.html

 

注:因内容太多,分为一、二两篇文章

 

 

类加载是java特性的一个很重要的部分。尽管,java中“advanced topics”的发展,使java的类加载机制地位有所下降。但每位编程者都应该知道这部分的工作机制,以及怎样去配合其工作。这可以使我们节省很多时间,而不必要浪费在调试ClassNotFoundExceptionClassCastException等。

 

这篇文章将从最基本的开始,比如代码和数据的关系,以及他们怎么样关系起来形成一个实例或者对象。然后将会说到,java中怎样通过类加载器把代码加载到JVM中,以及java中实现的主要的几种类型的类加载器。在这篇文章中,然后我们将会了解到java类加载机制的内幕,我们将使用最基本的代码来描述,这些代码执行于类加载器之后,但在加载一个类之前。在接下来的部分将使用一些例子来证实,对于开发者继承和开发自己的类加载器的必要性。接着将告诉你们怎样编写自己的类加载器,以及怎样使用它们去创建一个一般的能加载包括远程客户端辅助代码的类加载器引擎,以及怎样把它在JVM中定义,实例化,然后执行。习惯上,把J2EE-specific components 中说明的作为java类加载的规范,这篇文章正是从这本手册总结来的。

类和数据

一个类代表一段要执行的代码,然而数据则代表与这些代码相关联的某种状态。状态可以改变,代码不能改变。我们把一种特定状态与一个类关联起来时,就得到了这个类的一个实例。所以同一个类的不同实例有不同的状态,但都参照相同的代码。在java中,一个类通常它的代码就包含在一个 .class 文件中,虽然其中也包括异常。然而,在java运行时,每个类都会构造一个超类对象(first-class object),它们其实是java.lang.Class的实例。不论何时编译一个java文件,编译器都会在编译后的字节码中嵌入一个public, static, final型的字段class,这个字段表示的就是一个java.lang.Class型的实例。因为它是public类型的,我们可以通过标识符来访问它,像这样:

java.lang.Class klass = Myclass.class; 

只要一个类被加载到JVM,相同的类(强调:相同的类)将不会被重复加载。这将产生一个问题,什么才是相同的类?一个对象有一种特定状态和标识,对象总是与它所属类联系在一起,与这种状况相似,一个被加载到JVM中类也有特定的标识,接下来我们就阐述:

 

在java中,一个类通过认证的类全名来唯一标识。认证的类全名是由包名和类名两部分组成。但是在一个类被加载到JVM中则是通过认证的类全名,还有加载这个类的加载器来唯一标识。因此,一个类的类名为C1,包名为Pg,被类加载器类KClassLoader的一个实例k1加载,则C1,也就是C1.class ,的类实例,在JVM中将被解释为(C1,Pg,k1)。这就意味着两个不同的类加载器实(Cl, Pg, kl1) 和 (Cl, Pg, kl2) ,加载的类在JVM中将有不同的类实例对象,不是类型可比型(type-compatible)的。在JVM中有多少个类加载器实例呢?下面,我们将讲解这个。

类加载器

在java中,每个类都会被java.lang.ClassLoader的一个实例加载。ClassLoader类处于java.lang

包下面,开发者可以*的创建它的子类,添加自己功能的类加载器。

 

每当敲入java MyMainClass,一个新的JVM开始时,引导类加载器(bootstrap class loader )首先会把java中的一些关键类,像java.lang.Objent,和运行时的代码载入内存。这些运行时类打包在 JRE\lib\rt.jar文件中。因为是一个本地的接口,我们并不能从java文档中得到引导类加载器(bootstrap class loader )信息。也正是这个原因,引导类加载器(bootstrap class loader )的表现也根据JVM的不同而异。

比如,如果我们试图得到一个核心java运行时类的一个类加载器,我们将得到null值,如下:

log(java.lang.String.class.getClassLoader()); 

下面要说到的是java扩展类加载器。在java.ext.dirs 路径下面,我们可以放java扩展类库,这样我们可以获得超出java核心运行时类的特性。扩展类加载器(ExtClassLoader)将会加载java.ext.dirs目录下的所有 .jar 文件。开发者可以为自己的应用增加新的 .jar 文件 或者 类库,只要他把它们添加到java.ext.dirs目录下面以至于能被扩展类加载器找到。

在Sun的java指南中,文章“理解扩展类加载”(Understanding Extension Class Loading)对以上三个类加载器路径有更详尽的解释,这是其他几个JDK中的类加载器 

 

java.net.URLClassLoader 

java.security.SecureClassLoader 

java.rmi.server.RMIClassLoader 

sun.applet.AppletClassLoader 

 

java.lang.Thread,包含了public ClassLoader getContextClassLoader()方法,这一方法返回针对一具体线程的上下文环境类加载器。上下文加载器是线程创建着提供的,用以来加载线程运行时需要的类和资源。如果没有设定,默认的是父线程的上下文类加载器。最原始的上下文类加载器由加载application应用程序的类加载器建立。

类加载器怎样工作

所有的类加载器,除了引导类加载器,都一个父类加载器。而且,它们都是java.lang.ClassLoader类型的。上面两句话是不同的,而且对与开发者开发的任何一个类加载器的正常工作来说都非常重要。在这里,最重要的是怎样正确的设置父类加载器。类加载器的父类加载器实例会负责加载此类加载器类。(记住:一个类加载器本身也是一个类。)

在一个类加载器外部请求一个类时,使用loadClass() 方法。这个方法的具体工作,我们可以从源代码来看:

 

protected synchronized Class<?> loadClass 

    (String name, boolean resolve) 

    throws ClassNotFoundException{ 

 

    // First check if the class is already loaded 

    Class c = findLoadedClass(name); 

    if (c == null) { 

        try { 

            if (parent != null) { 

                c = parent.loadClass(name, false); 

            } else { 

                c = findBootstrapClass0(name); 

            } 

        } catch (ClassNotFoundException e) { 

            // If still not found, then invoke 

            // findClass to find the class. 

            c = findClass(name); 

        } 

    } 

    if (resolve) { 

     resolveClass(c); 

    } 

    return c; 

 

 

设置父类加载器,我们有两种方法,在ClassLoader的构造方法中。

public class MyClassLoader extends ClassLoader{ 

 

    public MyClassLoader(){ 

        super(MyClassLoader.class.getClassLoader()); 

    } 

 

 

或者

 

public class MyClassLoader extends ClassLoader{ 

 

    public MyClassLoader(){ 

        super(getClass().getClassLoader()); 

    } 

 

第一种方法更为常用,因为在构造方法中使用getClass() 方法是不提倡的,因为对象初始化仅在构造方法结束后才会完成。因此,如果正确设置了类加载器的父类加载器,不论什么时候从类加载器实例请求一个类时,如果此类加载器不能加载此类,则首先会交给它的父类加载器处理。如果此父类加载器也不能加载那个类,则又会交给上一层的父类加载器,依此类推。但是如果findBootstrapClass0()方法也未能加载那个类时,就会唤醒findClass()去处理。findClass()的默认的实现是抛ClassNotFoundException 异常。所以开发者需要继承java.lang.ClassLoader来实现用户自编写的类加载器。findClass()默认的实现如下:

protected Class<?> findClass(String name) 

        throws ClassNotFoundException { 

        throw new ClassNotFoundException(name); 

 

 

深入findClass()方法,类加载器需要从其它资源获取字节码。这些资源可以是文件系统、网络URL、数据库、其它的可以把字节码转换为流的应用程序,或者能够产生与java特性相适应的字节码的类似资源。你可以使用BCEL (Byte Code Engineering Library),它能非常方便的根据运行时的迹象创建类 。BCEL已经成功的应用在了一些地方,如编译器、优化程序、模糊程序(obsfuscators)、代码生成器、分析工具。只要这个这些字节码被重新获取,findClass()方法将会调用defineClass()方法,而且这时运行时(runtime )环境是非常特殊的对于具体哪一个类加载器实例去调用defineClass()这个方法。所有,两个类加载器实例从两个相同的、或者不同的资源加载字节码时,这些加载的类都是不同的。

在java语言详解(Java language specification)中,给出了在java执行引擎(Java Execution Engine)中加载、链接、初始化的类和接口等过程的详细解释。

 

图1 展示了一个带有main方法的应用程序类MyMainClass。正如前面所说的,MyMainClass.class 将被AppClassLoader加载,MyMainClass 创建两个加载器类实例,CustomClassLoader1和 CustomClassLoader2,他们都能从某些资源(比如说:网络)中加载第四个类Target 的字节码。这就意味着Target 这个类的定义超出了应用程序的class path 或者扩展class path 范围。在这种情况下,如果MyMainClass 让客户加载器实例去加载Target 类。Target 将同时被CustomClassLoader1和 CustomClassLoader2加载和定义。在java中这样就会有严重的问题。如果在Target 中包含一段静态(static)的初始化代码,如果我们要求这段代码执行且仅贝被执行一次,在我们目前的情况下,这段代码将被执行两次。在两个CustomClassLoader中,都执行了一次。如果Target 被两个CustomClassLoader同时初始化,他将会有两个实例target1 和target2 ,正如下面图1所示的,target1 和target2 是不可比的。也就是说,在java中不能执行这段代码:

Target target3 = (Target) target2; 

上面的代码将会抛出ClassCastException异常。这是因为JVM把他们看做两个独立的不同的类类型,因为它们被不同类型的ClassLoader 实例加载。如果MyMainClass 使用的不是不同类型的ClassLoader(这里:CustomClassLoader1 和CustomClassLoader2) 实例,使用同一类型的ClassLoader 的两个实例来加载,情况也将是一样的。这将在后面通过代码来证实。

 

译 java 中的类加载机制(1)

 

图1 在同一个JVM中多个ClassLoader加载同一个类Target

关于类加载、定义、链接的过程更加详细的解释在Andreas Schaefer的文章Inside Class Loaders中。

 

第二篇 

 

为什么我们要使用我们自己的类加载器?