JVM类加载器

来源:互联网 发布:python编写桌面程序 编辑:程序博客网 时间:2024/06/05 16:08

前言

在之前的文章中提过,一个对象包括三个部分:对象头、实例数据和对齐填充(非必须)。对象头包括标志位(Mark Word)、类型指针、数组长度(只针对数组类型)。其中的类型指针指向它的类元数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。

正题

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类一同确立其在Java虚拟机的唯一性,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:比较两个类是否“相等”,只有这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类必定不相等。

public class OOMTest {public static void main(String[] args) {            try{                //准备url                URL url = new File("C:/Users/Administrator/workspace/ClassTest/src").toURI().toURL();                URL[] urls = {url};                //定义类加载器                 URLClassLoader classLoader = new URLClassLoader(urls);            URLClassLoader classLoader2 = new URLClassLoader(urls);            //加载ClassA类            Class<?> outClass = classLoader.loadClass("com.memory.ClassA");             //使用同一个加载器再次加载同一个Class文件            Class<?> outClass2 = classLoader.loadClass("com.memory.ClassA");             //使用不同加载器加载同一个Class文件            Class<?> outClass3 = classLoader2.loadClass("com.memory.ClassA");             //查看使用同一个加载器多次加载同一个类的输出            System.out.println(outClass.equals(outClass2));              System.out.println(outClass.equals(outClass3));              classLoader.close();            classLoader2.close();        } catch(Exception e) {                e.printStackTrace();            }        }    }

运行结果:

truefalse

从结果可以看出:同一个加载器加载同一个Class文件,在虚拟机中指向的是同一个Class对象;不同加载器加载同一个Class文件,在虚拟机中就会存在多个Class对象。

这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。如果没有注意到类加载器的影响,在某些情况下可能会产生迷惑性的结果。

public class ClassLoaderTest {public static void main(String[] args) throws Exception {ClassLoader myLoader = new ClassLoader() {@Overridepublic Class<?> loadClass(String name)throws ClassNotFoundException {try {String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";InputStream is = getClass().getResourceAsStream(fileName);if(is == null) {return super.loadClass(name);}byte[] b = new byte[is.available()];is.read(b);return defineClass(name, b, 0, b.length);} catch (IOException e) {throw new ClassNotFoundException(name);}}};Object object = myLoader.loadClass("com.memory.ClassLoaderTest").newInstance();System.out.println(object.getClass());System.out.println(object instanceof com.memory.ClassLoaderTest);}}

运行结果:

class com.memory.ClassLoaderTestfalse

程序中使用了这个类加载器去加载一个名为“com.memory.ClassLoaderTest”的类,并实例化这个类的对象。两行输出结果中,从第一句可以看出,这个对象确实是类com.memory.ClassLoaderTest实例化出来的对象,但从第二句可以发现,这个对象与类com.memory.ClassLoaderTest做所属类型检查的时候却返回了false,这是因为虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载的,另一个是由我们自定义的类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类,做对象所属类型检查是结果自然为false。

在Java中,ClassLoader是一个抽象类,位于java.lang包中。下面对该类的一些重要接口方法进行介绍:

  • Class loadClass(String name):name参数指定类装载器需要装载类的名字,必须使用全限定类名,如com.memory.ClassLoaderTest。该方法有一个重载方法loadClass(String name,boolean resolve),resolve参数告诉装载器是否需要解析该类。在初始化该类之前,应考虑进行类解析工作,但并不是所有的类都需要解析。如果JVM只需要知道该类是否存在或找出该类的超类,那么就不需要进行解析。
  • Class defineClass(String name, byte[] b, int off, int len):将类文件的字节数组转换成JVM内部的java.lang.Class对象。字节数组可以从本地文件系统、远程网络获取。参数name为字节数组对应的全限定类名。
  • Class findSystemClass(String name):从本地文件系统载入Class文件,如果本地文件系统不存在该Class文件,则将抛出ClassNotFoundException异常。该方法是JVM默认使用的装载机制。
  • Class findLoadedClass(String name):调用该方法来查看ClassLoader是否已经装入某个类。如果已装入,那么返回java.lang.Class对象;否则返回null。如果强行装载已存在的类,那么将会抛出链接错误。
  • Class get:Parent():获取装载器的父类装载器。除根装载器外,所有的类装载器有且仅有一个父类装载器。ExtClassLoader的父类装载器是根装载器,因为根装载器非Java语言编写,所以无法获得,将返回null。

除JVM默认的3个ClassLoader外,用户可以编写自己的第三方类装载器,以实现一些特殊的需求。类文件被装载并解析后,在JVM内部拥有一个对应的java.lang.Class类描述对象,该类的实例都拥有指向这个类描述对象的引用,而类描述对象又拥有指向关联ClassLoader的引用:


每个类在JVM中都拥有一个对应的java.lang.Class对象,它提供了类结构信息的描述。数组、枚举、注解及基本Java类型(如Int、double等)甚至void都拥有对应的Class对象。Class没有public构造方法。Class对象是在装载类时由JVM通过调用类装载器中的defineClass()方法自动构造的。

双亲委派模型

从Java虚拟机的角度来讲,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一个就是所有其他的类加载器,这些类加载器都是由Java语言实现,独立于虚拟机外部,并且都继承自抽象类java.lang.ClassLoader。

从Java开发人员的角度来看,类加载器还可以划分为更细致一些,绝大部分Java程序都会使用到一下3种系统提供的类加载器。

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


看看java.lang.Class

     /**     * Returns the class loader for the class.  Some implementations may use     * null to represent the bootstrap class loader. This method will return     * null in such implementations if this class was loaded by the bootstrap     * class loader.      **/    public ClassLoader getClassLoader() {        ClassLoader cl = getClassLoader0();        if (cl == null)            return null;        SecurityManager sm = System.getSecurityManager();        if (sm != null) {            ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());        }        return cl;    }

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


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

我们的应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。这些类加载的关系如下图:


上图展示了类加载器之间的这种层次关系,称为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器之外,其余的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系来复用类加载器的代码。

类加载器的双亲委派模型在JDK1.2期间被引入并被广泛应用于之后几乎所有的Java程序中,但它并不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子类加载器才会尝试自己去加载。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。

双亲委派模型对于保证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()方法,若父类加载器为空则默认使用启动类加载器作为父类加载器。如果父类加载失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。调用findClass()时会先加载其继承的父类。

类加载器过程示例:

public class ClassA extends ClassB{}public class ClassLoaderTest extends ClassA{}
ClassLoaderTest类和ClassA加载图:


原创粉丝点击