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

简介: 手把手教你全面掌握-类加载机制(二)

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

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

image.pngimage.png从扩展类加载器和系统类加载器的继承关系图可以看出两者均是继承自java.lang.ClassLoader抽象类。因此介绍下ClassLoader中几个重要的方法:image.png在标准扩展类加载器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;
    }
}

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

image.png

二、双亲委派机制示例

1、创建测试beanimage.png3、将Person.class打包成test.jar复制到<JAVA_RUNTIME_HOME>\lib\ext目录下image.png再次运行代码测试,查看输出结果:sun.misc.Launcher$ExtClassLoader@7f31245a


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

image.png

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

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


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

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

image.png三、开发自己的类加载器

在类加载过程中,真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用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();
        }
    }
}

输出结果:image.png

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

使用接口

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


文章知识点与官方知识档案匹配,可进一步学习相关知识


目录
相关文章
|
算法 Java 应用服务中间件
由浅入深理解JVM虚拟机
由浅入深理解JVM虚拟机
74 0
|
7月前
|
存储 监控 数据可视化
技术小白应该知道的关于JVM的一些事
技术小白应该知道的关于JVM的一些事
|
IDE Java 开发工具
如何学习Java核心知识
如何学习Java核心知识
|
算法 Oracle Java
温故知新-JVM篇
温故知新-JVM篇
45 0
|
存储 缓存 监控
JVM关键知识点整理,从入门到提高到实践
Java 虚拟机定义了各种在程序执行期间使用的运行时数据区域。这些数据区域有一些是在Java虚拟机启动时创建的,并在Java虚拟机退出时销毁,有一些数据区域是每个线程独有的,在线程创建时创建,在线程销毁时销毁,根据《Java虚拟机规范》的规定,Java虚拟机运行时所需要管理的数据区域主要如下图所示:
347 0
JVM关键知识点整理,从入门到提高到实践
类加载的思维导图
类加载的思维导图
类加载的思维导图
|
SQL Java 数据库
Java虚拟机开发与实践专栏介绍
Java虚拟机开发与实践专栏介绍
|
存储 Java 编译器
JVM 从入门到精通(一)初窥Java虚拟机
JVM 从入门到精通(一)初窥Java虚拟机
126 0
JVM 从入门到精通(一)初窥Java虚拟机
|
缓存 Java BI
JVM从入门到入土之实战JVM调优(一)
前言 文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820… 种一棵树最好的时间是十年前,其次是现在
158 0
|
Java
JVM从入门到入土之实战JVM调优(二)
前言 文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…
136 0