千家信息网

java中的Class装载系统ClassLoader是怎样使用

发表于:2025-02-03 作者:千家信息网编辑
千家信息网最后更新 2025年02月03日,java中的Class装载系统ClassLoader是怎样使用,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。ClassLoader在
千家信息网最后更新 2025年02月03日java中的Class装载系统ClassLoader是怎样使用

java中的Class装载系统ClassLoader是怎样使用,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。

ClassLoader在Java中有着非常重要的作用,它主要工作是在Class装载的加载阶段,主要作用是从系统外部获得Class二进制数据流

1. 认识ClassLoader

ClassLoader是java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将ClassLoader在整个装载阶段,只能影响类的加载,而无法通过ClassLoader改变类的连接和初始化行为。

从代码层面上看,ClassLoader是一个抽象类,它提供了一些重要的接口,用于自定义Class的加载流程和加载方式。ClassLoader的主要方法如下:

  • public Class loadClass(String name) throws ClassNotFoundException:给定一个类名,加载一个类,返回代码这个类的Class实例,如果找不到类,则返回ClassNotFoundException异常。

  • protected final Class defineClass(byte[] b, int off, int len):根据给定的字节码流b定义一个类,offlen参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是一个受保护的方法,只有在自定义ClassLoader子类中可以使用。

  • protected Class findClass(String name) throws ClassNotFoundException:查找一个类,这是一个受保护的方法,也是重载ClassLoader时重要的系统扩展点。这个方法在loadClass()中被调用,用于自定义查找类的逻辑。如果不需要修改类加载默认机制,只是想改变类加载的形式,就可以重载该方法。

  • protected final Class findLoadedClass(String name):这也是一个受保护的方法,它会寻找已经加载的类。这个方法是final方法,无法被修改。

ClassLoader的结构中,还有一个重要的字段:parnet。它也是一个ClassLoader的实例,这个字段所表示的ClassLoader称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader可能会将某些请求交给自己的双亲处理。

2. ClassLoader的分类

在标准的java程序中,java虚拟机会创建3类ClassLoader为整个应用程序服务。它们分别是:Bootstrap ClassLoader(启动类加载器)、Extension ClassLoader(扩展类加载器)和 App ClassLoader(应用类加载器,也称系统类加载器)。此外每一个应用程序还可以拥有自定义的 ClassLoader,以扩展java虚拟机获取Class数据的能力。

ClassLoader层次结构如下图所示。当系统需要使用一个类时,在判断类是否已经被加载时,会从底层类加载器开始进行判断。当系统需要加载一个类时,会从顶层类开始加载,依次向下尝试,直到成功。

  • 启动类加载器:完全由c语言实现,并且java中没有对象与之对应,负责加载系统的核心类,比如rt.jar中的java类。

  • 扩展类加载器:用于加载%JAVA_HOME%/lib/ext/*.jar中的java类。

  • 应用类加载器:用于加载用户类,也就是用户程序的类。

  • 自定义类加载器:用于加载一些特殊途径的类,一般也是用户程序的类。

下列代码输出了加载的类加载器:

public class Demo04 {    public static void main(String[] args) {        ClassLoader cl = Demo04.class.getClassLoader();        while (cl != null) {            System.out.println(cl.getClass().getName());            cl = cl.getParent();        }    }}

代码中先取得装载当前类Demo04ClassLoader,然后打印当前ClassLoader并获得其双亲,直到类加载器树被遍历完成。运行结果如下:

sun.misc.Launcher$AppClassLoadersun.misc.Launcher$ExtClassLoader

由此得知,Demo03是由AppClasLoader(应用类加载器)加载的,而AppClassLoader的双亲为ExtClassLoader(扩展类加载器)。从ExtClassLoader无法再取得启动类加载器,因为这是一个系统级的纯C语言实现。因此,任何启动类加载器中加载的类是无法获得其ClassLoader实例的,比如:

String.class.getClassLoader()

由于String属于java核心类,会被启动类加载器加载,故以上代码返回的是null.

3. ClassLoader 的双亲委托模式

系统中的ClassLoader在协同工作时,默认会使用双亲委托模式。在类加载的时候,系统会判断当前类是否已经被加载,如果已经被加载,就会直接返回可用的类,否则就会尝试加载。在尝试加载时,会请求双亲处理,如果请求失败,则会自己加载。

以下代码显示了ClassLoader加载类的详细过程,它在ClassLoader.loadClass()中实现:

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 {                        c = findBootstrapClassOrNull(name);                    }                } catch (ClassNotFoundException e) {                    // 如果双亲不为null                    // 在双亲加载不成功时,抛出ClassNotFoundException                }                if (c == null) {                    // 如果双亲加载不成功                    // 使用findClass查找类                    long t1 = System.nanoTime();                    c = findClass(name);                    // 定义类加载器,记录数据                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);                    sun.misc.PerfCounter.getFindClasses().increment();                }            }            if (resolve) {                resolveClass(c);            }            return c;        }    }

判断类是否加载时,应用类加载器会顺着双亲路径往上判断,直到启动类加载器。但是启动类加载器不会往下询问,这个委托是单向的。

4. 双亲委托模式的弊端

由前面的分析可知,检查类是否已加载的委托过程是单向。这种方式虽然从结构上比较清晰,使用各个ClassLoader的职责非常明确,但是会带来一个问题:即上层的ClassLoader无法访问下层的ClassLoader所加载的类,如下图:

通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中为应用类。按照这种模式,应用类访问系统类自然没问题,但是系统类访问应用类就会出现问题。比如,在系统类中提供了一个接口,该接口需要在应用中得以实现,还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这些就会出现该工厂方法无法创建由应用类加载器的应用实例的问题

5. 双亲委托模式的补充

在java平台中,通常把核心类(rt.jar)中提供外部服务、可由应用层自行实现的接口称为Service Provider Interface,即SPI.

下面以javax.xml.parsers中实现XML文件解析功能模块为例,说明如何在启动类加载中访问由应用类加载器实现的SPI接口实例。

public static DocumentBuilderFactory newInstance() {    return FactoryFinder.find(            /* The default property name according to the JAXP spec */            DocumentBuilderFactory.class, // "javax.xml.parsers.DocumentBuilderFactory"            /* The fallback implementation class name */            "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl");}

FactoryFinder.find()函数试图加载并返回一个DocumentBuilderFactory实例。当这个实例在应用层jar包时,它会使用如下方法进行查找:

Object provider = findJarServiceProvider(factoryId);

其中factoryId就是字条串javax.xml.parsers.DocumentBuilderFactoryfindJarServiceProvider的主要内容如下代码所示(这段代码并非jdK中的源码,为了展示主要功能,做了删减):

private static Object findJarServiceProvider(String factoryId) throw ConfigurationError {        String serviceId = "META-INF/services" + factoryId;        InputStream is = null;        ClassLoader cl = ss.getContextClassLoader();        InputStream is = ss.getResourceAsStream(cl, serviceId);        BufferedReader rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));        String factoryClassName = rd.readLine();        return newInterface(factoryClassName, cl, false, useBSClsLoader);}

从以上代码可知,系统通过读取jar包中META-INF/services目录下的类名文件读取工厂类类名,然后根据类名生成对应的实例,并将此ClassLoader传入newInstance()方法,由这个ClassLoader完成实例的加载和创建,而不是由这段代码所在的启动类加载品加载。从而解决了启动类加载器无法访问factoryClassName指定类的问题。

以上代码中,加载工厂类方法略有曲折,我们平时写代码时,知道了一个类的包名.类名,要生成该类的对象,通常是这么进行的:

  1. Class.forname("包名.类名"),拿到Class对象。

  2. 拿到Class对象后,调用Class.newInstance()方法,生成该对象的实例。

但是,在DocumentBuilderFactory中,这样做就行不通了,主要原因在于Class.forName()无法拿到类加载器。我们来看看Class.forName()的源码:

public static Class forName(String className)            throws ClassNotFoundException {    Class caller = Reflection.getCallerClass();    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);}

从上面可以看到,在哪个类里调用了Class.forName(),就使用加载那个类的类加载器进行类加载,即DocumentBuilderFactory调用了Class.forName(),就使用加载DocumentBuilderFactory的类加载器进行加载包名.类名,但问题是DocumentBuilderFactory是由BootClassLoader加载的,获取到的类加载器是null,这是无法加载包名.类名

6. 突破双亲模式

双亲模式的类加载方式是虚拟机默认的行为,但并非必须这么做,通过重载ClassLoader可以修改该行为。下面将演示如何打破默认的双亲模式:

package jvm.chapter10;import java.io.ByteArrayOutputStream;import java.io.File;import java.io.FileInputStream;import java.nio.ByteBuffer;import java.nio.channels.Channels;import java.nio.channels.FileChannel;import java.nio.channels.WritableByteChannel;class MyClassLoader extends ClassLoader {    private String fileName;    public MyClassLoader(String fileName) {        this.fileName = fileName;    }    @Override    protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {        Class re = findClass(name);        if (re != null) {            return re;        }        System.out.println("load class " + name + " failed, parent load start");        return super.loadClass(name, resolve);    }    @Override    protected Class findClass(String className) throws ClassNotFoundException {        Class clazz = this.findLoadedClass(className);        if (null == clazz) {            try {                String classFile = getClassFile(className);                FileInputStream fis = new FileInputStream(classFile);                FileChannel fileChannel = fis.getChannel();                ByteArrayOutputStream baos = new ByteArrayOutputStream();                WritableByteChannel outChannel = Channels.newChannel(baos);                ByteBuffer buffer = ByteBuffer.allocateDirect(1024);                while (true) {                    int i = fileChannel.read(buffer);                    if (i == 0 || i == -1) {                        break;                    }                    buffer.flip();                    outChannel.write(buffer);                    buffer.clear();                }                fis.close();                byte[] bytes = baos.toByteArray();                clazz = defineClass(className, bytes, 0, bytes.length);            } catch (Exception e) {                e.printStackTrace();            }        }        return clazz;    }    private String getClassFile(String packageName) {        return fileName + packageName.replaceAll("\\.", File.separator) + ".class";    }}/** * {这里添加描述} * * @author chengyan * @date 2019-11-29 4:12 下午 */public class Demo05 {    public static void main(String[] args) throws Exception {        MyClassLoader myClassLoader = new MyClassLoader("/Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/");        Class clz = myClassLoader.loadClass("jvm.chapter10.Demo01");        System.out.println(clz.getClassLoader().getClass().getName());        System.out.println("=======class load tree===========");        ClassLoader cl = clz.getClassLoader();        while(cl != null) {            System.out.println(cl.getClass().getName());            cl = cl.getParent();        }    }}

以上代码通过自定义ClassLoader重载loadClass()方法,改变了默认的委托双亲加载的方式,运行结果如下:

java.io.FileNotFoundException: /Users/chengyan/IdeaProjects/myproject/DataStructuresAndAlgorithms/out/production/DataStructuresAndAlgorithms/java/lang/Object.class (No such file or directory)        at java.io.FileInputStream.open0(Native Method)        at java.io.FileInputStream.open(FileInputStream.java:195)        at java.io.FileInputStream.(FileInputStream.java:138)        at java.io.FileInputStream.(FileInputStream.java:93)        at jvm.chapter10.MyClassLoader.findClass(Demo05.java:36)        at jvm.chapter10.MyClassLoader.loadClass(Demo05.java:22)        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)        at java.lang.ClassLoader.defineClass1(Native Method)        at java.lang.ClassLoader.defineClass(ClassLoader.java:763)        at java.lang.ClassLoader.defineClass(ClassLoader.java:642)        at jvm.chapter10.MyClassLoader.findClass(Demo05.java:52)        at jvm.chapter10.MyClassLoader.loadClass(Demo05.java:22)        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)        at jvm.chapter10.Demo05.main(Demo05.java:76)load class java.lang.Object failed, parent load startjvm.chapter10.MyClassLoader=======class load tree===========jvm.chapter10.MyClassLoadersun.misc.Launcher$AppClassLoadersun.misc.Launcher$ExtClassLoader

可以看到,程序首先试图由MyClassLoader加载Object类,但由于指定的路径中没有该类信息,故加载失败,抛出异常,但随后就由应用类加载器加载成功。接着尝试加载Demo01Demo01在指定的路径中,加载成功。打印加载Demo01ClassLoader,显示为MyClassLoader,打印ClassLoader层次,依次为MyClassLoaderAppClassLoaderExtClassLoader.

关于java中的Class装载系统ClassLoader是怎样使用问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注行业资讯频道了解更多相关知识。

0