JVM系列:(七)JVM类加载器

111 阅读6分钟

一 什么是类加载器

通过一个类的全限定名去获取描述此类的二进制字节流的动作被称为类加载器。这个动作被放到了Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。

二 类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类加载阶段。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。也就是比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类是来源同一个class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。

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

三 类加载模型

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

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

  1. 启动类加载器(Bootstrap ClassCloader):这个类加载器负责将存放在<JAVA_HOME>\lib 目录中的,或者被-Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中,启动类加载器无法被Java程序直接引用。

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

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

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

上图展示的类加载器之间的这种层次关系。被称为类加载器的双亲委派模型。双亲委派模型要求除了最顶层的启动类加载器Bootstrap ClassLoader 外,其余所有类加载器都有父类加载器。这里的类加载器之间的父子关系,不是通过继承关系实现,而是通过组合关系实现的。

双亲委派的工作原理:

  1. 应用程序类加载器收到了类加载请求,首先不会自己尝试加载,而是调用父类扩展类加载器Extension ClassLoader去加载。

  2. 扩展类加载器收到类加载请求,首先不会自己尝试加载,而是继续调用父类启动类加载器Bootstrap ClassLoader去加载。

  3. 如果启动类加载器加载失败,例如在 <JAVA_HOME>/jre/lib 中没找到,会使用子类扩展类加载器Extension ClassLoader去加载。

  4. 若扩展类加载器Extension ClassLoader也加载失败,则会使用应用程序类加载器去加载。

  5. 若应用程序类加载器也加载失败,则会抛出ClassNotFoundException异常。

双亲委派类加载模型的好处在于:

  • 系统类防止内存中出现多份同样的字节码。

  • 保证Java程序安全稳定运行。

四 实现自己的类加载器

实现自己的类加载器,主要就是通过给定的class路径,获取class字节码,再调用父类应用程序类加载的双亲委派模型加载class字节码的过程。class路径可以是本地文件路径,也可以是网络文件路径,我们这次实现的是本地文件路径:

public class MyClassLoader extends ClassLoader {        // 重写父类的获取 class 字节码方法
    @Override        protected Class<?> findClass(String name) throws ClassNotFoundException {                byte[] classData = loadClassData(name);                if (classData == null) {                        throw new ClassNotFoundException();
        } else {                        return defineClass(name, classData, 0, classData.length);
        }
    }                    // 加载本地文件指定路径的 class 字节码
    private byte[] loadClassData(String className) {                String fileName = "/User/qbian/" + className.replace('.', File.separatorChar) + ".class";                try {
            InputStream is = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();                        int bufferSize = 1024;                        byte[] buffer = new byte[bufferSize];                        int length = 0;                        while ((length = is.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }                                    return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }                return null;
    }                public static void main(String[] args) {
        MyClassLoader myClassLoader = new MyClassLoader();                try {                                   // 加载 /User/qbian/me/qbian/Test.class 类
            Class<?> testClass = myClassLoader.findClass("me.qbian.Test");
            Object test = testClass.newInstance();

            System.out.println("加载测试类的加载器为 = " + test.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

五 总结

hotspot 虚拟机的类加载模型主要是双亲委派模型,可以做到系统的稳定安全,也可以自定义自己的类加载器去自定义自己的加载方式,需要注意的是不要破坏了双亲委派模型。


接下来我们将介绍:

  • JVM内存模型