手记

深入JAVA虚拟机之类加载器

前言:
虚拟机设计团队把类加载阶段中"通过一个类的权限定名来获取描述此类的二进制字节流"这个动作放到虚拟机外部区实现,让程序自己决定如何去获取所需的类。实现这个动作的代码模块就被称为类加载器

它最初是为了满足Java Applet的需求而被开发,而现在Java Applet基本已经宣布死亡,但类加载器却在类层次划分、OSGi、热部署、代码加密等领域大放异彩。它是java技术体系中一块重要的基石。


对于任意一个类,都需要由加载该类的加载器和这个类本身一同确立在虚拟机中的唯一性。通俗说就是:比较两个类是否相等,这两个类必须是同一个类加载器的前提才有意义。否则,就算两个类是来源同一个class文件,只要加载器不同,那么这两个类必定是不相等的。

备注:
这里所指的"相等", 包括:Class对象的equals方法、isAssignableForm方法、isInstance方法的返回结果,也包括instanceof关键字做对象所属关系判定等情况。

双亲委派模型

在虚拟机的角度看,只有两类类加载器:启动类加载器、其他类加载器。启动类加载器是属于虚拟机中的一部分,是用C++实现的,而其他类加载器则是java实现的。在开发者眼中看来,类加载器可以分的更加细致些:
启动类加载器:
这个类加载器,负责把放在JAVA_HOME\lib目录中的,或者被-Xbootclasspath参数指定的路径中的,并且是虚拟机识别的类库加载到虚拟机中。启动类加载器无法被java程序直接引用。

扩展类加载器:
它是负责JAVA_HOME\lib\ext目录中的,或者被java.ext.dirs系统变量指定的所有类库,开发者可以直接使用扩展类加载器。

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

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

双亲委派模型,规定了,每一个类加载器(除了启动类加载器)都必须有父类,并且这种关系不是继承实现,而是通过组合实现的,而且要求当调用了当前类加载器时,必须先给父类去加载,如果没有父类,那么就交给启动类加载器加载,如果父类返回无法加载,则给当前类加载器加载,如果当前也无法加载,则抛出ClassNotFoundException.

这样做的好处,就会有一个优先级,确保一个类只会被一个类加载器加载如虚拟机中,如果都是优先当前,那么就会可能出现同一个类在虚拟机中被不同的类加载器加载了多遍,并且这多个类还是不相等的。

双亲委派的代码都在java.lang.ClassLoader的loadClass方法中,我们可以通过分析源代码就可以知道:

/**     * Loads the class with the specified <a href="#name">binary name</a>.  The     * default implementation of this method searches for classes in the     * following order:     *     * <ol>     *     *   <li><p> Invoke {@link #findLoadedClass(String)} to check if the class     *   has already been loaded.  </p></li>     *     *   <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method     *   on the parent class loader.  If the parent is <tt>null</tt> the class     *   loader built-in to the virtual machine is used, instead.  </p></li>     *     *   <li><p> Invoke the {@link #findClass(String)} method to find the     *   class.  </p></li>     *     * </ol>     *     * <p> If the class was found using the above steps, and the     * <tt>resolve</tt> flag is true, this method will then invoke the {@link     * #resolveClass(Class)} method on the resulting <tt>Class</tt> object.     *     * <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link     * #findClass(String)}, rather than this method.  </p>     *     * <p> Unless overridden, this method synchronizes on the result of     * {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method     * during the entire class loading process.     *     * @param  name     *         The <a href="#name">binary name</a> of the class     *     * @param  resolve     *         If <tt>true</tt> then resolve the class     *     * @return  The resulting <tt>Class</tt> object     *     * @throws  ClassNotFoundException     *          If the class could not be found     */    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;        }    }

破坏双亲委派模型
双亲委派模型不是一个强制性的约束模型,只是java设计者们推荐给开发者的类加载器实现方式。绝大部分都是遵循这个模型的。但截止到深入java虚拟机作者编写这本书时就已经有三次破坏该模型。

第一次:
是出现在双亲委派模型之前,因为jdk1.2之后才引入双亲委派模型。而类加载器和抽象类java.lang.ClassLoader早在jdk1.0就出现了。为了兼容双亲委派模型之前的类加载器,java设计团队不得不妥协,给jdk1.2之后的java.lang.ClassLoader类新添加了一个protected的findClass方法,在此之前,用户去继承java.lang.ClassLoader就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用类加载器的 一个私有方法loadClassInternal(),而此方法唯一逻辑就是调用自己的loadClass()方法。前面通过loadClass源码,我们可以知道如果父类加载失败,才会去调用findClass()方法定义的类加载过程。这样就可以保证满足双亲委派模型。

第二次:
第二次是因为双亲模型自身的缺陷,虽然双亲委派模型很好的解决了各个类加载器的基础类的统一,但如果基础类又要调用回用户的代码呢?比如说:JDNI服务,JDNI服务是java的标准服务,它的代码是有启动类加载器加载,但JDNI的目的是为了对资源进行集中管理和查找,它需要调用其他独立厂商实现并部署在应用程序的ClassPath下的JDNI接口提供者的代码,但启动类并不认识这些代码。为了解决这类问题,java设计团队,提供了一种并不优雅的设计:线程上下文类加载器。这个类加载器可以通过java.lang.Thread类的setContextCLassLoader()方法进行设置,如果没有创建线程时设置,它会从父线程中继承一个,如果应用程序全局都没有设置,那么这个类加载器就是应用程序类加载器。

JDNI服务使用了这个线程上下文加载器去加载所需的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作。java中设计SPI的加载动作基本都采用这种方式,比如:JDNI、JDBC、JAXB、JCE和JBI等。

第三次:
第三次是由于用户对程序动态性的追求导致的,这里说的动态性是指:代码热替换、模块热部署等,说白了就是希望应用程序能像电脑外设那样,插上鼠标,键盘,不用重启机器。热部署对于企业生产环境的诱惑是毋庸置疑的。

0人推荐
随时随地看视频
慕课网APP