Java虚拟机--类加载器

作者: 贾博岩 | 来源:发表于2018-05-13 08:57 被阅读357次

    如何加载一个Class文件

    在之前的文章中,笔者介绍了Java虚拟机--类加载机制,阐述了一个类加载到底做了哪些事情!

    但是,关于类加载器的只是,并没有做任何介绍,只是说了下会在后面的文章中进行单独阐述。那么,本篇的意义就是来告诉大家类加载器的实现。

    首先,我们来简单的回顾下类加载机制中的内容。

    类加载机制

    虚拟机把类的数据从.class文件加载到内存,并对class文件中的数据进行校验、转换、解析、初始化等操作后,最终形成可以被虚拟机识别并使用的Class对象的过程就叫做“虚拟机的类加载”,主要包括为3大阶段。

    类加载机制

    阶段一:加载

    加载,类加载器通过类的全限定名来获取类的二进制字节流,获取的方式可以通过jar包、war包、网络、JSP文件中获取,绝大部分情况下是通过jar包、war包中获取。

    获取到字节流后,会将字节流中的信息转化为方法区中的运行时数据结构。在内存中,生成代表该类的Class对象,作为访问该类的数据入口。

    阶段二:连接

    连接比较复杂,分为3个小阶段:

    验证:确保被加载类的正确性,即确保被加载的类符合javac编译的规范,可编译通过的代码。

    准备:为类的静态变量分配内存,并初始化为默认值(零值)。

    解析:将类中的符号引用转化为直接引用。

    阶段三:初始化

    为类的静态变量赋值,与连接阶段中的的准备不同。此阶段,代码可debug查看。

    如int类型的静态变量static int x = 3,连接阶段赋零值即为0,而初始化阶段赋值即为3。

    以上就是类加载机制的三大阶段,而我们今天要将的类加载器存在于阶段一中--加载。可以说,没有类加载器也就没有了后续的流程,类加载器在Java虚拟机中起到了至关重要的作用。

    类加载器

    类加载器(class loader)将Java类从本地磁盘加载到Java虚拟机中,并同时创建了该类的Class对象,实现了“通过一个类的全限定类名来获取此类的二进制字节流”功能。

    类加载器是Java语言的一项创新,也是Java语言流程的重要原因之一,在类层次划分、OSGI、热部署、代码加密等领域有着重要的作用,成为Java不可或缺的一部分。

    首先,我们来写一个测试类,来看下类加载器,ClassLoaderTest测试类:

    public class ClassLoaderTest { 
       public static void main(String[] args) { 
           ClassLoader loader = ClassLoaderTest.class.getClassLoader(); 
           while (true) { 
               System.out.println(loader);
                if(loader==null){
                    break;
                }
                loader = loader.getParent();
           } 
       } 
    }
    

    运行结果:

    sun.misc.Launcher$AppClassLoader@41dee0d7
    
    sun.misc.Launcher$ExtClassLoader@f7b650a
    
    null
    

    首先获取到的是AppClassLoader类加载器,紧接着又获取的是ExtClassLoader类加载器,最后获取的对象为null

    为什么为null呢,后续来解答!

    接下来,我们来看看在Java体系中到底有哪些类加载器。

    类加载的分类

    在Java中,类加载器可以分为两大类,一类是由Java系统提供的,另外一类是自定义的,由开发人员编写提供的。

    系统类加载器:

    引导类加载器(bootstrap class loader):用来加载Java的核心库,由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作,不继承自java.lang.ClassLoader(这就是上面例子中为什么最后取到的对象为null的原因)。负责加载<Java_Runtime_Home>/lib下面的类库到内存中,或-Xbootclasspath选项指定的jar包装入内存;

    扩展类加载器(extensions class loader):用来加载Java的扩展库,由sun.misc.Launcher$ExtClassLoader来实现。负责加载
    <Java_Runtime_Home>/lib/ext,或-Djava.ext.dirs选项指定目录下的jar包装入到内存中;

    系统类加载器(system class loader):用来加载Java应用的类路径(CLASSPATH)的Java类,由sun.misc.Launcher$AppClassLoader来实现。一般来说,Java应用中的类都是由它来完成加载的,可以通过ClassLoader.getSystemClassLoader()来获取。

    自定义类加载器:

    自定义类加载器(User Custom ClassLoader):开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。在程序运行期间, 通过自定义的java.lang.ClassLoader子类动态加载class文件。

    java.lang.ClassLoader类介绍

    方法 说明
    getParent() 返回该类加载器的父类加载器。
    loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
    findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
    findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
    defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
    resolveClass(Class<?> c) 链接指定的 Java 类。

    以上为ClassLoader对于类加载功能的主要方法介绍。

    在我们的应用程序中,都是由这4种类加载器互相配合进行加载,这4种类加载器在虚拟机中维护了一种父子关系,这种关系叫做“双亲委派模型”。下面,我们就来看看什么是双亲委派模型。

    双亲委派模型

    下面的图片中,展示的就是“双亲委派模型”,模型中呈现出Java体系架构中的四大类加载器的关系,除了顶层的引导类加载器之外,其余类加载都需要有父加载器存在,但是此子父类关系并不是通过java代码中继承的方式实现。具体如何实现,后面讲解。

    1526024942(1).png

    知道了类加载器的结构模型,那么该模型在代码整个Java体系中如何工作呢?

    工作流程:一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个类加载请求委派给其父类加载器去完成,每一个层的类加载器都是如此,依次向父类加载器传递,最终所有的类加载请求都会传送到顶层的启动类加载器(bootstrap)中,只有当父加载器反馈无法完成这个类加载请求时,子类加载器才会尝试自己去进行类加载操作,如果子类加载器也依旧无法完成,则代码层面就会抛出异常。

    此时,你会不会感到疑惑?为啥儿子自己的活不去干,而首先交给他爹去完成呢?这么做的目的何在?

    在Java体系中,双亲委派模型保证了类的唯一性,将Java类与它的类加载器绑定到了一起,当父类加载器加载完成后,子类加载器不会再次加载。此外,双亲委派模型还保证了Java框架的安全性。例如:java.lang.Object类,无论是上述哪个类加载器要加载这个类,最终都会委派给模型中的启动类加载器去加载,因此java.lang.Object类在程序中保证了唯一性。

    相反,如果没有使用该模型,而是由各个类加载器自行去加载的话,那么系统中就会出现不同的java.lang.Object类,类的唯一性被打破,Java体系中的基本行为就得不到保证。例如:,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等。涉及到“类相等”的方法有:Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法以及instanceof对象所属关系判定。

    试想一下,如果我们自定义一个java.lang.Object类会怎么样?(其实我们自定义的java.lang.Object类无法在程序中被导入,只能模拟定义java.lang.Object类--java.lang.ObjectTest)

    当JVM请求类加载进行自定义的类加载时,双亲委派模型会将请求传递到启动类加载器中,但是启动类加载器默认只加载<Java_Runtime_Home>/lib路径下的类,在该路径下并没有ObjectTest类,所以启动类加载器无法加载,只能向下传递给子类加载器,最终会将请求传递到系统类加载器中,但是系统类加载器也无法进行加载,会抛出异常。

    为什么,why?

    这是因为以java.开头的是核心API包,需要访问权限,强制加载会抛出异常,任何以java.开头的包都会报错:

    Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
    
    image

    此异常是代码层面抛出的,并不是native方法虚拟机底层抛出,源码可见(ClassLoader类):

    if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
    }
    

    此时,你会不会又突发奇想,我自己定义一个类,放在<Java_Runtime_Home>/lib路径下会如何?

    image

    编写代码,并打成jar包,jar包的名称就叫做 jiaboyan.jar:

    jar cvf jiaboyan.jar com\jiaboyan\test\ObjectTest.class
    

    接下来,把jiaboyan.jar包放入到<Java_Runtime_Home>/lib下,也同时放入到<Java_Runtime_Home>/lib/ext,并同时保留截图中的代码,如图所示:

    image

    执行main()方法,结果如下:

    sun.misc.Launcher$AppClassLoader@8fd9b4d
    

    从输出可以看出,放置到<Java_Runtime_Home>/lib目录下的ObjectTest.class类并没有被启动类加载器加载,而是由扩展类加载器加载了。

    why?不是说了委派给最顶层的类加载进行加载吗?其实,这是由于虚拟机出于安全角度考虑,不会加载<Java_Runtime_Home>/lib中的陌生类,开发者把自定义的类放置到此目录下启动类加载器是不会进行加载的。

    下一篇,将会对类加载器源码进行分析!!!

    相关文章

      网友评论

        本文标题:Java虚拟机--类加载器

        本文链接:https://www.haomeiwen.com/subject/nvbkdftx.html