Java虚拟机之类加载机制详解(二)

时间:2022-12-21 09:56:15

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。

类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。类加载主要有以下几个作用:

  • 通过一个类的全限定名来获取定义此类的二进制字节流;
  • 审查每一类应该有谁加载,它是一种父优先的等级加载机制;
  • 将Class字节码重新解析成JVM统一要求的对象格式。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都有一个独立的类名称空间。即比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里说的相等,包括equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

双亲委派模型

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

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

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
  • 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  • 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现,这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,如果应用程序中,没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

Java应用程序都是由这3中类加载器互相配合进行加载的,如果有必要,可以加入自己定义的类加载器,这些加载器的关系一般如下所示:

Java虚拟机之类加载机制详解(二)

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

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

这种模式的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar之中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,若是没有双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在ClassPath中,那系统中将会出现多个不同的Object类,应用程序将会变得一片混乱。而且,自己编写一个包名为java.lang的类,将会发现可以正常编译,但永远无法被加载运行。如下所示:

package java.lang;

public class Test {

	public static void main(String[] args) {
		// TODO Auto-generated method stub
		System.out.println("我是java.lang.Test类");
	}

}
运行结果:
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
	at java.lang.ClassLoader.preDefineClass(Unknown Source)
	at java.lang.ClassLoader.defineClass(Unknown Source)
...

双亲委派模型的实现

除了启动类加载器,所有的类加载器都会继承ClassLoader类,在类加载过程中,主要会用到如下几个方法,以及它们的重载方法。

Java虚拟机之类加载机制详解(二)

双亲委派模型在Java中的实现很简单,其代码都在java.lang.ClassLoader类的loadClass(String)方法中,源码如下:

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

loadClass(String)方法,调用了它的重载方法loadClass(String, boolean),其中第二个boolean参数用来指示是否连接刚刚加载的类,默认为false。

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;
    }
}
上述代码主要做了以下事情:

1、调用findLoadedClass(String)方法来检查是否已经加载过该类,若没有加载过,则进入下一步,否则跳转到第四步;

2、调用父加载器的loadClass(String)方法,若父加载器为空则默认使用启动类加载器作为父加载器,若父加载器加载失败,则抛出ClassNotFoundException异常,进入下一步,否则跳转到第四步;

3、调用自己的findClass(String)方法进行加载,然后进入下一步;

4、若需要连接该类,则调用resolveClass(Class<?>)方法。

java.lang.ClassLoader类的findClass(String)方法默认实现为:

protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

所以我们需要覆盖ClassLoader父类的findClass()方法来实现类的加载机制,从而取得要加载类的字节码。然后调用defineClass()方法生成类的Class对象,如果你想在类被加载到JVM中时就被连接(Link),那么可以接着调用另外一个resolveClass()方法,当然也可以选择让JVM来解决什么时候才连接这个类。

ClassLoader是个抽象类,它有很多子类,如果我们要实现自己的类加载器,一般可以继承URLClassLoader这个子类,因为这个类已经帮我们实现了大部分工作,我们只需在适当的地方进行修改就好。

但是不管你是直接实现抽象类ClassLoader,还是继承URLClassLoader类,或者其他类,在默认情况下,自己创建的类加载器都会调用getSystemClassLoader()方法,将其返回值作为自己的父加载器。

通过源码可以发现getSystemClassLoader()方法会调用initSystemClassLoader()方法来初始化系统类加载器scl:

scl = l.getClassLoader();
其中,l为sun.misc.Launcher对象,getClassLoader()源码为:
public ClassLoader getClassLoader() {
    return loader;
}

loader变量就是系统类加载器,它的初始化过程在Launcher类的构造函数中:

private ClassLoader loader;

public Launcher() {
    // Create the extension class loader
    ClassLoader extcl;
    try {
        extcl = ExtClassLoader.getExtClassLoader();
    } catch (IOException e) {
        throw new InternalError(
            "Could not create extension class loader", e);
    }

    // Now create the class loader to use to launch the application
    try {
        loader = AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
        throw new InternalError(
            "Could not create application class loader", e);
    }
    ...
}

所以,自己创建的类加载器的父加载器就是AppClassLoader,这也符合了虚拟机的双亲委派模型。

破坏双亲委派模型

双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。在Java程序中大部分的类加载器都遵循这个模型,但是也有例外。

双亲委派模型第一次被破坏是在双亲委派模型出现之前,即JDK 1.2发布之前,在此之前,继承java.lang.ClassLoader的唯一目的就是重写loadClass()方法,因为虚拟机在进行类加载时会调用加载器的私有loadClassInternal()方法,而该方法就是调用loadClass()方法。

而在JDK 1.2之后,为了向前兼容,java.lang.ClassLoader添加了一个protected方法findClass(),这时,已不再提倡用户去覆盖loadClass()方法,而应该把自己的类加载逻辑写到findClass()方法中,以符合双亲委派模型。

双亲委派模型第二次被破坏是由模型自身的缺陷导致的,在该模型中,越基础的类由越上层的类加载器进行加载,但是基础类要调用用户的代码怎么办?

为了解决这个问题,Java虚拟机引进了一个新的类加载器:线程上下文类加载器(Thread Context ClassLoader),可以通过Thread类的setContextClassLoader()方法进行设置。如果创建线程时还未设置,它将会从父类继承一个,如果应用程序在全局范围都没有设置过的话,那默认就是应用程序类加载器。

这时,父类加载器就可以调用子类加载器去完成类加载的动作,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用类加载器。

双亲委派模型第三次被破坏是由于对程序动态性的追求而导致的,如代码热替换、模块热部署等,在此情况下,类加载器不再是双亲委派模型,而是进一步发展为更加复杂的网状结构。

参考资料

周志明:《深入理解Java虚拟机》

许令波:《深入分析Java Web技术内幕》