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

发布时间:2021-09-27 09:49:59 作者:柒染
来源:亿速云 阅读:173

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

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

1. 认识ClassLoader

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

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

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

2. ClassLoader的分类

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

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

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

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

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$AppClassLoader
sun.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所加载的类,如下图:

java中的Class装载系统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.<init>(FileInputStream.java:138)
	at java.io.FileInputStream.<init>(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 start
jvm.chapter10.MyClassLoader
=======class load tree===========
jvm.chapter10.MyClassLoader
sun.misc.Launcher$AppClassLoader
sun.misc.Launcher$ExtClassLoader

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

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

推荐阅读:
  1. 在Java的反射中Class.forName和ClassLoader有哪些区别
  2. java中什么是class

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

java class classloader

上一篇:JAVA如何实现连接本地打印机并打印文件

下一篇:如何在CentOS系统的服务器上用ss5配置socket5代理

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》