Java虚拟机----类加载器

时间:2022-12-27 12:52:38

. 类加载器概述

        任何一个类型在使用前,都必须经历完整的加载、连接和初始化3个类加载步骤,一旦类型成功经历了这3个步骤之后,它就可以随时随地的被使用了,开发人员可以在程序中访问和调用它的静态类成员信息(比如:静态字段、静态方法),或者使用new关键字为其创建对象实例。当然从一个类型被加载到JVM中开始算起,直至最终被卸载出内存为止,它的整个生命周期也就随之结束了。

        类加载是JVM执行类加载机制的前提。简单的说,类加载器的主要任务就是根据一个类的全限定名来读取此类的二进制字节流到JVM内部,然后转化为一个与目标类对应的java.lang.Class对象实例。类加载器最早出现在Java1.0版本中,那个时候只是单纯的满足Java Applet应用而被研发出来,但如今类加载器却在OSGi、字节码加解密领域大放光彩。

        一般情况下,Java开发人员并不需要在程序中显式的使用类加载器,但是了解类加载器的加载机制却显得至关重要。相信大家都遇到过java.lang.ClassNotFoundException异常或者java.lang.NoClassDefFoundError异常,所以为了避免在这类异常出现的时候发生手足无措的情况,了解类加载机制对于Java开发人员来说尤为重要,因为这样才能在出现异常的时候根据错误异常日志定位问题和解决问题。除此之外,在一些特殊的应用场景中,比如需要支持类的动态加载或需要对编译后的字节码文件进行加密操作时,就需要与类加载器打交道了,也就是说,开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。

        参考《Java虚拟机规范(Java SE7版)》的描述,JVM支持两种类型的类加载器,分别是引导类加载器()和自定义类加载器()。在此大家需要注意:从概念上讲,自定义类加载器一般指的是程序中由开发人员自定义的类加载器,但是Java虚拟机规范却没有这样定义,而是将所有继承自抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器始终只有3种类型,如下所示:

1.Bootstrap ClassLoader(引导类加载器)

2.Extension ClassLoader(扩展类加载器)

3. AppClassLoader/System ClassLoader(系统类加载器)

        Bootstrap ClassLoader也叫启动类加载器,它是由C++语言编写并嵌套在JVM内部,主要负责加载“JAVA_HOME/lib”目录下的所有类型,或者有选项“-Xbootclasspath”指定路径中的所有类型。Extension ClassLoaderAppClassLoader继承自抽象类ClassLoader,并且都是采用Java语言进行编写的,前者主要负责加载“JAVA_HOME/lib/ext”扩展目录下的所有类型,而后者主要是负责加载ClassPath目录下的所有类型。除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。如图1所示,最下面一级CustomClassLoader是开发人员自定义类加载器。

 Java虚拟机----类加载器

                                                                                  图1  类加载器分类

二、抽象类ClassLoader

        在程序中一些自定义的类加载器来重新定义类的加载规则,以便实现一些自定义的处理逻辑。在程序中编写一个自定义类加载器是一件非常简单的任务,只需要继承抽象类ClassLoader并重写其findClass()方法即可。当编写好自定义的类加载器后,便可以在程序中调用loadClass()方法来实现类加载操作。

        java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java类,即 java.lang.Class类的一个实例。除此之外,ClassLoader还负责加载Java应用所需的资源,如图像文件和配置文件等。不过本文只讨论其加载类的功能。为了完成加载类的这个职责,ClassLoader提供了一系列的方法,比较重要的方法如 1所示。关于这些方法的细节会在下面进行介绍。

方法

说明

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类中的getParent()方法用于获取当前类加载器的父类加载器。按照双亲委派模型的规则,除了引导类加载器Bootstrap之外,所有的类加载器都有一个父类加载器。对于系统提供的类加载器来说,AppClassLoader系统类加载器的父类加载器是Extension 扩展类加载器,而扩展类加载器的父类加载器是Bootstrap引导类加载器;对于开发人员编写的自定义类加载器来CustomClassLoader说,其父类加载器是AppClassLoader

三、双亲委派模型

        双亲委派模型又叫做“类加载器的代理模式”。规则如下:当一个类加载器接收到一个类加载任务的时候,它并不会立即展开加载,而是将加载任务委派给他的父类加载器去执行,每一层的类加载器都采用相同的方式,直至委派给最高顶层的Bootstrap ClassLoader启动类加载器为止。如果父类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器去执行加载。     

        使用双亲委派模型的优点是能够有效的确保一个类的全局唯一性,当程序中出现多个全限定名相同的类时,类加载器在执行加载的时候,始终都只会加载其中某一个类,不会2个类都加载,如果想通过defindClass()方法进行显式加载,JVM会抛出异常。为了加深理解,特地找了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 {//否则不存在父类加载器,则直接委派给最顶层Bootstrap启动类加载器执行加载

 

                        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;

        }

    }

        以上代码,除其中的中文注释外,均为JDK8.0官方文档ClassLoader.class文件内容,可自行参看。JDK8.0的和JDK7.0的略有不同。

        上述代码中,首先会由findLoadedClass(name)方法检查目标类型之前是否已经被成功加载过了,如果没有被加载过,则调用父类加载器的loadClass()方法,将加载任务委派给父类加载器;如果不存在父类加载器,则通过findBootstrapClassOrNull()方法直接委派给最顶层Bootstrap启动类加载器执行加载。如果父类加载器抛出ClassNotFoundException异常,则意味着父类加载器无法完成加载,则只能自行加载

        在此需要注意,由于Java虚拟机规范并没有明确要求类加载的加载机制一定要使用双亲委派模型,只能建议采用这种方式而已。比如在Tomcat中,类加载器所采用的加载机制和传统的双亲委派模型就有一定的区别,当缺省的类加载器接收到一个加载任务的时候,首先会自行加载,当它加载失败后,才会将类的加载任务委派给它3的父类加载器去执行,这同时也是Servlet规范推荐的一种做法。

四、自定义类加载器

        一般情况下,Java开发人员并不需要在程序中显式的使用自定义类加载器CustomClassLoader,但是在一些特殊的应用场景中,如果当前的类加载器无法满足我们的需要时,就需要在程序中自定义类加载器来重新定义类的加载规则,以便实现自定义的处理逻辑。比如,当一个字节码文件在编译的时候进行了加密处理,那么在被类加载器执行加载的时候,首先要做的就是解密,否则类加载器就会认为这个字节码文件不符合JVM规范,它不是一个有效的字节码文件。想要满足这些特殊的应用场景,就需要自定义类加载器。

        需要在程序中实现一个自定义的类加载器是非常简单的。只需要继承抽象类ClassLoader并重写其findClass()方法即可。在此大家需要注意,尽管Java虚拟机规范将所有派生自抽象类ClassLoader的类加载器都划分为自定义类加载器,但从严格意义上来说,由Java开发人员编写的类加载器并不属于Java体系结构的组成部分,实际上它仅仅是属于Java运行时程序的一部分而已。演示代码如下:

package com.ljheee.loader;

import java.io.BufferedInputStream;

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

/**

 * CustomClassLoader

 * @author ljheee

 *

 */

public class MyClassLoader extends ClassLoader{

      private String byteCode_Path;

 

  public MyClassLoader(String byteCode_Path) {

        this.byteCode_Path = byteCode_Path;

  }

  @Override

  protected Class<?> findClass(String name) throws ClassNotFoundException {

        byte value[] = null;

        BufferedInputStream in = null;

        try {

            in = new BufferedInputStream(new FileInputStream(byteCode_Path+name+".class"));

            value = new byte[in.available()];

           in.read(value);

         } catch (FileNotFoundException e) {

             e.printStackTrace();

         } catch (IOException e) {

             e.printStackTrace();

        } finally{//释放资源

           try {

              if(null != in)  in.close();

               } catch (IOException e) {

                    e.printStackTrace();

              }

       }

         //byte数组转化为一个类的Class对象实例

        return defineClass(value, 0, value.length);

  }

  public static void main(String[] args) throws ClassNotFoundException {

        MyClassLoader loader = new MyClassLoader("E:\\GitCode\\MyClassLoader\\bin\\com\\ljheee\\loader\\");

        System.out.println("当前类的父类加载器"+loader.getParent().getClass().getName());

        System.out.println("加载目标类的类加载器"+loader.loadClass("Test").getClassLoader().getClass().getName());

  }

}

注意Main方法中指定的Test类,是之前新建的测试类,其字节码文件在参数指定的路径下。

程序输出:

当前类的父类加载器sun.misc.Launcher$AppClassLoader

加载目标类的类加载器com.ljheee.loader.MyClassLoader

完整源码:https://github.com/ljheee/MyClassLoader

 

推荐文章《Java虚拟机----类的加载过程》