手把手教你全面掌握-类加载机制

Posted 李子捌

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手把手教你全面掌握-类加载机制相关的知识,希望对你有一定的参考价值。

一、双亲委派机制的介绍与分析

JVM在加载类时,默认采用的是双亲委派机制,通俗讲,就是某个特定的类的类加载器在接收到加载类的请求时,首先将加载任务委托给父类加载器,依次递归(本质上是loadClass函数的递归调用),因此所有的请求最终都会传送到顶层的启动类加载器中。如果父类加载器可以完成这个加载请求,就成功返回;如果父类加载器无法完成加载请求,子类才会尝试自己加载。事实上,大多数情况下,越基础的类由上层加载器加载,因为这些类往往被用户代码经常调用(当然也存在基础类回调用户代码的情况,即破坏双亲委派机制的情形)。接下来我们从系统类加载器和扩展类加载器作为例子简单分析虚拟机默认的双亲委派机制。

ExtClassLoader(扩展类加载器)继承关系图:
在这里插入图片描述

AppClassLoader(系统类加载器)继承关系图:
在这里插入图片描述

从扩展类加载器和系统类加载器的继承关系图可以看出两者均是继承自java.lang.ClassLoader抽象类。因此介绍下ClassLoader中几个重要的方法:

// 加载指定权限定类名的二进制类型,供用户调用
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

// 加载指定权限定类名的二进制类型,指定是否解析(resolve参数不一定能真正达到解析的效果),供继承用
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException{
	// ToDo
}

// findClass方法一般被loadClass方法调用去加载指定名称类,供继承用
 protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
 }

// 定义类型,一般在findClass方法中读取到对应字节码后调用,final修饰,不嫩被继承
// JVM已经实现了具体的功能,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用即可。
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
        throws ClassFormatError{
        return defineClass(name, b, off, len, null);
}

在标准扩展类加载器ExtClassLoader和系统类加载器AppClassLoader以及两者的公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码中,均没有对java.lang.ClassLoader中的加载委派规则loadClass方法。因此我们可以从ClassLoader中的loadClass方法的源码中分析虚拟机默认的双亲委派机制的原理

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

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 根据类路径获取锁 
    synchronized (getClassLoadingLock(name)) {
        // 判断该类是否已经被加载了
        Class<?> c = findLoadedClass(name);
        // 如果未被加载
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如果存在父加载器,委托给父加载器加载
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 调用本地方法findBootstrapClass() BootStrap类加载器加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
			// 如果加载器未加载
            if (c == null) {
               
                long t1 = System.nanoTime();
                 // 调用findClass方法,实则调用defineClass方法,通过自身加载器加载,如果无法加载则抛出ClassNotFundException
                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;
    }
}

由上面的代码我们引发一个思考,系统类加载器的父类加载器是扩展类加载器,扩展类加载器的父类加载器是启动类加载器,是否真的是这样呢?我们通过代码来测试一下:

/**
 * <p>
 *       测试类加载器之间的关系
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/31 21:14
 */
public class ClassLoaderRelationshipTest {

    public static void main(String[] args) {
        
        System.out.println(ClassLoader.getSystemClassLoader());
        System.out.println(ClassLoader.getSystemClassLoader().getParent());
        System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
        
    }
}

输出结果:
在这里插入图片描述

通过上述的测试代码和输出结果,可以非常明确的看出ClassLoader.getSystemClassLoader()可以直接获取系统类加载器,而通过ClassLoader.getSystemClassLoader().getParent()可以看出系统类加载器的父类加载器是扩展类加载器,但是ClassLoader.getSystemClassLoader().getParent().getParent()的输出结果为null,是否说明我们的猜想存在问题呢?事实上,由于启动类加载器无法直接通过Java代码获取,他是在虚拟机中实现的,JVM默认采用null来代表启动类加载器。这个点我们可以通过ClassLoader的构造函数中知晓。

// parent设置为私有属性,并且未提供设置接口(Setter方法)
private final ClassLoader parent;

// getSystemClassLoader()方法为获取系统类加载器
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

// 强制设置父类加载器
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}

// 在不指定明确的父类加载器时,设置parent加载器为系统类加载器
private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        domains = Collections.synchronizedSet(new HashSet<ProtectionDomain>());
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        domains = new HashSet<>();
        assertionLock = this;
    }
}

二、双亲委派机制示例

1、创建测试bean

package com.liziba.classloader.bean;

public class Person {

    private String name;

}

2、当前工程中创建测试类

package com.liziba.classloader;

public class ClassLoaderRuleTest {

    public static void main(String[] args) {
        try {
            // 查看Java 类路径
            System.out.println(System.getProperty("java.class.path"));
            // 调用加载当前类的类加载器加载测试类ClassLoaderRuleTest
            Class<?> clazz = Class.forName("com.liziba.classloader.ClassLoaderRuleTest");
            // 查看加载当前测试类的类加载器
            System.out.println(clazz.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

输出结果:
在这里插入图片描述

3、将Person.class打包成test.jar复制到<JAVA_RUNTIME_HOME>\\lib\\ext目录下

在这里插入图片描述

再次运行代码测试,查看输出结果:sun.misc.Launcher$ExtClassLoader@7f31245a

由上可以证明前面说的双亲委派机制:系统类加载器在接收到加载请求时,首先将请求委派给父类加载器(标准扩展类加载器)进行加载,而在上面的示例中扩展类加载器抢先加载类Person.class的加载请求。

在这里插入图片描述

4、将test.jar复制到<JAVA_RUNTIME_HOME>\\lib目录下

在这里插入图片描述

输出结果:
在这里插入图片描述

第四步和第三步输出的结果是一致的,Person.class的加载请求都有扩展类加载器加载,这和前面所说的双亲委派机制并不矛盾。JVM出于安全考虑,不会加载<JAVA_HOME>/lib目录下存在的陌生类,只能加载JVM指定的类。

5、删除<JAVA_RUNTIME_HOME>\\lib\\ext的test.jar和当前目录下编译的Person.class

输出结果:系统抛出java.lang.ClassNotFoundException

在这里插入图片描述

三、开发自己的类加载器

在类加载过程中,真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用defineClass来实现的;而启动类的加载过程是通过调用loadClass来实现的。前者称为一个类的定义加载器(defining loader),后者称为初始加载器(initiating loader)。在Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。方法 loadClass()抛出的是 java.lang.ClassNotFoundException异常;方法 defineClass()抛出的是 java.lang.NoClassDefFoundError异常。类加载器在成功加载某个类之后,会把得到的 java.lang.Class类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

1、文件系统类加载器

package com.liziba.classloader;

import java.io.*;

/**
 * <p>
 *      文件系统类加载器
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/31 23:04
 */
public class FileSystemClassLoader extends ClassLoader {

    /** 指定文件路径 */
    private String rootDir;

    public FileSystemClassLoader(String rootDir) {
        this.rootDir = rootDir;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] data = getClassByteData(name);
        if (data == null || data.length == 0) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, data, 0, data.length);
        }
    }


    /**
     * 读取类的字节流、获取字节数组
     *
     * @param className
     * @return
     */
    private byte[] getClassByteData(String className) {
        String path = classNameCovertToPath(className);
        try {
            InputStream in = new FileInputStream(path);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024 * 4];
            int len = 0;
            while ((len = in.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            return baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }


    /**
     * 类权限定名转绝对路径
     *
     * @param className
     * @return
     */
    private String classNameCovertToPath(String className) {
        return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
    }

}

测试类

package com.liziba.classloader;


import com.liziba.classloader.bean.Person;

/**
 * <p>
 *      测试文件类加载器
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/31 23:12
 */
public class TestFileSystemClassLoader {

    public static void main(String[] args) {

        String rootDir = "E:\\\\workspaceall\\\\liziba-java\\\\out\\\\production\\\\liziba-java";
        String className = "com.liziba.classloader.bean.Person";
        FileSystemClassLoader fscl = new FileSystemClassLoader(rootDir);
        Class<?> clazz = null;
        try {
            clazz = fscl.findClass(className);
            Object object = clazz.newInstance();
            System.out.println(object);
            System.out.println(object.getClass().getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }
}

输出结果:
在这里插入图片描述

2、网络类加载器

package com.liziba.classloader;

import sun.nio.ch.Net;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;

/**
 * <p>
 *      网络类加载器
 * </p>
 *
 * @Author: Liziba
 * @Date: 2021/5/31 23:25
 */
public class NetworkClassLoader extends ClassLoader{

    /** 指定网络URL */
    private String rootUrl;

    public NetworkClassLoader(String rootUrl) {
        this.rootUrl = rootUrl;
    }


    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = getClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }


    /**
     * 从网络上获取类的字节数组
     *
     * @param className
     * @return
     */
    private byte[] getClassData(String className) {

        String path = classNameCovertToPath(className);
        try {
            URL url = new URL(path);
            InputStream ins = url.openStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024 * 4];
            int len = 0;
            // 读取类文件的字节
            while ((len = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, len);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 类权限定名转绝对路径
     *
     * @param className
     * @return
     */
    private String classNameCovertToPath(String className) {
        return rootUrl + "/" + className.replace('.', '/') + ".class";
    }
}

网络类加载器加载后,一般有两种办法来使用这个类

  • 使用Java反射API
  • 使用接口

具体的使用过程和上面的文件类加载器的使用大同小异,相信聪明的大家也不需要在演示啦!

以上是关于手把手教你全面掌握-类加载机制的主要内容,如果未能解决你的问题,请参考以下文章

手把手教你构建 Android WebView 的缓存机制 & 资源预加载方案

手把手教你构建 Android WebView 的缓存机制 & 资源预加载方案

手把手教你掌握4类数据清洗操作

手把手教你BSCHECO智能合约通缩机制燃烧分红销毁代币合约部署

怎么用106短信平台来发送营销短信?手把手教你轻松掌握

手把手教你阅读mybatis核心源码,掌握底层工作原理与设计思想