类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。

注意:JVM主要在程序第一次主动使用类的时候,才会去加载该类,也就是说,JVM并不是在一开始就把一个程序就所有的类都加载到内存中,而是到不得不用的时候才把它加载进来,而且只加载一次。

类加载器

jvm支持两种类型的加载器,分别是引导类加载器和 自定义加载器

  • 引导类加载器是由c/c++实现的
  • 自定义加载器是由java实现的。

jvm规范定义自定义加载器是指派生于抽象类ClassLoder的类加载器。按照这样的加载器的类型划分,在程序中我们最常见的类加载器是:引导类加载器BootStrapClassLoader、自定义类加载器(Extension Class Loader、System Class Loader、User-Defined ClassLoader)

上图中的加载器划分为包含关系而并非继承关系

启动类加载器

这个类加载器使用c/c++实现,嵌套再jvm内部,用来加载Java的核心类库(JAVA_HOME/jre/lib/rt.jar、resource.jar或sun.boot.class.path路径下的内容),用于提供JVM自身需要的类。并不继承自java.lang.ClassLoader,没有父加载器

扩展类加载器

java语言编写,由sun.misc.Launcher$ExtClassLoader实现。从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext 子目录(扩展目录)下加载类库。如果用户创建的JAR 放在此目录下,也会自动由扩展类加载器加载;派生于 ClassLoader。 父类加载器为启动类加载器

系统类加载器

java语言编写,由 sun.misc.Lanucher$AppClassLoader 实现。该类加载是程序中默认的类加载器,一般来说,Java应用的类都是由它来完成加载的,它负责加载环境变量classpath或系统属性java.class.path 指定路径下的类库;派生于 ClassLoader 。父类加载器为扩展类加载器 。通过ClassLoader#getSystemClassLoader() 方法可以获取到该类加载器。

public class ClassLoaderTest {
    public static void main(String[] args) {
        // 获取系统类加载器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 获取系统类加载器的父类加载器,扩展类加载器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader); // sun.misc.Launcher$ExtClassLoader@1b6d3586

        // 获取扩展类加载器的上层启动类加载器,这里获取不到
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader); // null

        // 获取用户自定义类的加载器,classLoader的打印结果和systemClassLoader的结果完全一致
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader); // sun.misc.Launcher$AppClassLoader@18b4aac2

        // 核心类库使用的是启动类加载器,以String为例
        ClassLoader stringClassLoader = String.class.getClassLoader();
        System.out.println(stringClassLoader);  // null
    }
}

双亲委派模型

双亲委派模型工作过程是:如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时(即 ClassNotFoundException ),子加载器才会尝试自己去加载。

为什么需要双亲委派模型?

假设没有双亲委派模型,试想一个场景:

黑客自定义一个 java.lang.String 类,该 String 类具有系统的 String 类一样的功能,只是在某个函数稍作修改。比如 equals 函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到 JVM 中。此时,如果没有双亲委派模型,那么 JVM 就可能误以为黑客自定义的java.lang.String 类是系统的 String 类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的 java.lang.String 类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的 java.lang.String 类,最终自定义的类加载器无法加载 java.lang.String 类。

或许你会想,我在自定义的类加载器里面强制加载自定义的 java.lang.String 类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在 JVM 中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

举个简单例子:

ClassLoader1 ClassLoader2 都加载 java.lang.String 类,对应Class1、Class2对象。那么 Class1对象不属于 ClassLoad2 对象加载的 java.lang.String 类型。

如何实现双亲委派模型?

双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实 ClassLoader 类默认的 loadClass 方法已经帮我们写好了,我们无需去写。

几个重要函数

loadClass 默认实现如下:

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

再看看 loadClass(String name, boolean resolve) 函数:

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;
    }
}

从上面代码可以明显看出, loadClass(String, boolean) 函数即实现了双亲委派模型!整个大致过程如下:

  1. 首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再加载,直接返回。
  2. 如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器,则由父加载器加载(即调用 parent.loadClass(name, false); ).或者是调用 bootstrap 类加载器来加载。
  3. 如果父加载器及 bootstrap 类加载器都没有找到指定的类,那么调用当前类加载器的 findClass 方法来完成类加载。

换句话说,如果自定义类加载器,就必须重写 findClass 方法!

findClass 的默认实现如下:

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

可以看出,抽象类 ClassLoader 的 findClass 函数默认是抛出异常的。而前面我们知道, loadClass 在父加载器无法加载类的时候,就会调用我们自定义的类加载器中的 findeClass 函数,因此我们必须要在 loadClass 这个函数里面实现将一个指定类名称转换为 Class 对象.

如果是读取一个指定的名称的类为字节数组的话,这很好办。但是如何将字节数组转为 Class 对象呢?很简单,Java 提供了 defineClass 方法,通过这个方法,就可以把一个字节数组转为Class对象

defineClass 主要的功能是:

将一个字节数组转为 Class 对象,这个字节数组是 class 文件读取后最终的字节数组。如,假设 class 文件是加密过的,则需要解密后作为形参传入 defineClass 函数。

defineClass 默认实现如下:

protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    throws ClassFormatError {
    return defineClass(name, b, off, len, null);
}

自定义加类加载器

为什么要自定义类加载器

  1. 隔离加载类

模块隔离,把类加载到不同的应用选中。比如tomcat这类web应用服务器,内部自定义了好几中类加载器,用于隔离web应用服务器上的不同应用程序。

  1. 修改类加载方式

除了Bootstrap加载器外,其他的加载并非一定要引入。根据实际情况在某个时间点按需进行动态加载。扩展加载源比如还可以从数据库、网络、或其他终端上加载

  1. 防止源码泄漏

java代码容易被编译和篡改,可以进行编译加密,类加载需要自定义还原加密字节码。

自定义类加载器实现

实现方式:

所有用户自定义类加载器都应该继承ClassLoader类

在自定义ClassLoader的子类是,我们通常有两种做法:

  1. 重写loadClass方法(是实现双亲委派逻辑的地方,修改他会破坏双亲委派机制,不推荐)
  2. 重写findClass方法 (推荐)

首先,我们定义一个待加载的普通 Java 类: Test.java 。

public class ClassLoaderTest {
    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader("d:/");
        try {
            Class<?> clazz = classLoader.loadClass("TestMain");
            System.out.println("我是由"+clazz.getClassLoader().getClass().getName()+"类加载器加载的");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

接下来就是自定义的类加载器:

import java.io.*;

public class MyClassLoader extends ClassLoader{

    private String codePath;

    public MyClassLoader(ClassLoader parent, String codePath) {
        super(parent);
        this.codePath = codePath;
    }

    public MyClassLoader(String codePath) {
        this.codePath = codePath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            //1.字节码路径
            String fileName = codePath+name+".class";
            //2.获取输入流
            bis = new BufferedInputStream(new FileInputStream(fileName));
            //3.获取输出流
            baos = new ByteArrayOutputStream();
            //4.io读写
            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1){
                baos.write(data , 0 , len);
            }
            //5.获取内存中字节数组
            byte[] byteCode = baos.toByteArray();
            //6.调用defineClass 将字节数组转成Class对象
            Class<?> defineClass = defineClass(null, byteCode, 0, byteCode.length);
            return defineClass;
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            try {
                baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

最后运行结果如下:

我是由 class Main$MyClassLoader 加载进来的

评论