专栏首页编码前线ClassLoader解析(一):Java中的ClassLoader

ClassLoader解析(一):Java中的ClassLoader

概述

ClassLoader(类加载器)的功能是将 class文件加载到JVM虚拟机中,让程序可以正确运行;但是,JVM启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载,不然,一次性加载那么多class,会占用大量内存。

ClassLoader的类型

Java中的类加载器主要有两种类型:系统类加载器和自定义类加载器。其中系统类加载器包括3中,分别是 BootstrapClassLoaderExtensionsClassLoaderAppClassLoader

Bootstrap ClassLoader

启动类加载器,是Java类加载层次中最顶层的类加载器,是用C/C++实现的,负责加载JDK中的核心类库,如 rt.jarresources.jarcharsets.jar等。可以通过启动JVM时指定 -Xbootclasspath来改变Bootstrap ClassLoader的加载目录。

获取该类加载器从哪些地方加载了相关的jar或class文件:

// 方式一
System.out.println(System.getProperty("sun.boot.class.path"));
// 方式二
URL[] urls = Launcher.getBootstrapClassPath().getURLs();
for (int i = 0; i < urls.length; i++) {
    System.out.println(urls[i].toExternalForm());
}

运行结果:

file:/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib/resources.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib/rt.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib/sunrsasign.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib/jsse.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib/jce.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib/charsets.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib/jfr.jar
file:/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/classes

Extensions ClassLoader

扩展类加载器,负责加载Java的扩展类库,默认加载 Java_home/jre/lib/ext目录下的所有jar。也可以通过 -Djava.ext.dirs选项指定目录。

获取该类加载器的加载目录:

System.out.println(System.getProperty("java.ext.dirs"));

运行结果:

/Users/xch/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_111.jdk/Contents/Home/jre/lib/ext:
/Library/Java/Extensions:
/Network/Library/Java/Extensions:
/System/Library/Java/Extensions:/usr/lib/java

App ClassLoader

负责加载当前应用程序classpath目录下的所有jar和class文件。也可以通过 -Djava.class.path加载的路径。

Custom ClassLoader

除了系统提供的类加载器,还可以自定义类加载器,自定义类加载器是通过继承 java.lang.ClassLoader类的方式来实现自己的类加载器。

ClassLoader的继承关系

获取一个类加载涉及到的类加载器:

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

运行结果:

sun.misc.Launcher$AppClassLoader@135fbaa4
sun.misc.Launcher$ExtClassLoader@2503dbd3

说明:

  • 第1行说明加载ClassLoaderTest的类加载器是AppClassLoader;
  • 第2行说明APPClassLoader的父加载器为ExtClassLoader;
  • 因为Bootstrap ClassLoader是由C/C++编写的,并不是一个Java类,所有无法在Java代码中获取它的引用。
  • ClassLoader是一个抽象类,其中定义了ClassLoader的主要功能。
  • SecureClassLoader继承了抽象类ClassLoader,但SecureClassLoader并不是ClassLoader的实现类,而是扩展了ClassLoader类加入了权限方面的功能,加强了ClassLoader的安全性。
  • URLClassLoader继承自SecureClassLoader,用来通过URI路径从jar文件和文件夹中加载类和资源。
  • ExtClassLoader和AppClassLoader都继承自URLClassLoader,它们都是Launcher的内部类,Launcher是Java虚拟机的入口应用,ExtClassLoader和AppClassLoader都是在Launcher中进行初始化的。

ClassLoader加载类的原理

原理介绍

类加载器查找Class所采用的是 双亲委托模式,所谓的双亲委托就是首先判断该Class是否已经加载,如果没有则不是自身去查找,而是委托给父加载器进行查找,一样依次的进行递归,直到委托到最顶层的Bootstrap ClassLoader,如果Bootstrap ClassLoader找到了该Class,就直接返回;如果没有找到,则继续依次向下查找;如果还没找到则最后会交给自身去查找。

加载过程

自底向上检查类是否已经加载;

Step1::自定义类加载器首先从缓存中查找Class是否已经加载,如果已将加载就返回该Class;如果没加载,则委托给父加载器也就是App ClassLoader。

Step2:按照上图中红色虚线的方向递归步骤1.

Step3:一直委托到Bootstrap ClassLoader,如果Bootstrap ClassLoader在缓存中还没找到Class,则在自己规定路径 JAVA_HOME/jre/lib中或者 Xbootclasspath选项指定路径的jar包中进行查找,如果找到,则返回该Class;如果没有,则交给子加载器Extensions ClassLoader。

Step4:Extensions ClassLoader查找 JAVA_HOME/jre/lib/ext目录下或者 -Djava.ext.dirs选项指定目录下的jar包;如果找到就返回,找不到则交给App ClassLoader。

Step5:App ClassLoader查找 classpath目录下或者 -Djava.class.path选项所指定目录下的jar包或class文件,如果找到就返回,找不到就交给自定义的类加载器,如果还找不到则抛出异常。

// ClassLoader中的loadClass方法
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    // 检查class是否已经被加载
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                // 调用父类加载器的loadClass方法
                c = parent.loadClass(name, false);
            } else {
                // 调用Bootstrap ClassLoader查找
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {
            // ClassNotFoundException thrown if class not found
            // from the non-null parent class loader
        }

        if (c == null) {
            // 向上委托没有找到该类,则调用findClass方法继续向下查找。
            c = findClass(name);
        }
    }
    return c;
}

双亲委托模式的好处

  1. 避免重复加载,如果已经加载过一次Class,就不需要再次加载,而是从缓存中直接读取。
  2. 更加安全,如果不使用双亲委托模式,就可以自定义一个String类来替代系统的String类,这会造成安全隐患,采用双亲委托模式会使String类在虚拟机启动时就被Bootstrap ClassLoader加载,所以用户自定义的ClassLoader永远无法加载一个自己写的String,除非改变JDK中ClassLoader搜索类的默认算法。并且只有两个类名一致并且被同一个类加载器加载的类,Java虚拟机才会认为它们是同一个类。

自定义ClassLoader

系统提供的类加载器只能加载指定目录下的jar包和class文件,如果想要加载网络上或者其他地方的jar包或者class文件则需要自定义ClassLoader。

自定义步骤

Step1:编写一个类继承自ClassLoader抽象类。

Step2:重写 findClass()方法。

Step3:在 findClass()方法中调用 defineClass()

说明

findClass()方法中定义查找class的方法,然后将class数据通过 defineClass()生成Class对象。

自定义示例

示例:自定义一个NetworkClassLoader,用于加载网络上的class文件

Step1. 创建NetworkClassLoader.java

public class NetworkClassLoader extends ClassLoader {

    private String rootUrl;

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

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

    private byte[] getClassData(String name) {
        InputStream is = null;

        try {
            String path = classNameToPath(name);
            URL url = new URL(path);
            byte[] buff = new byte[1024 * 4];
            int len;
            is = url.openStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while ((len = is.read(buff)) != -1) {
                baos.write(buff, 0, len);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    private String classNameToPath(String name) {
        return rootUrl + "/" + name.replace(".", "/") + ".class";
    }

}

Step2. 创建用于网络加载的类NetworkClassLoaderTest.java

package com.github.xch168.network;

public class NetworkClassLoaderTest {

    public void sayHello() {
        System.out.println("Hello from network class.");
    }
}

Step3. 将NetworkClassLoaderTest.java编译成NetworkClassLoaderTest.class并上传到七牛的对象存储服务器。

Step4. 创建测试类ClassLoaderTest.java

public class ClassLoaderTest {

    public static void main(String[] args) {
        try {
            String rootUrl = "http://pkx097e6d.bkt.clouddn.com";
            NetworkClassLoader networkClassLoader = new NetworkClassLoader(rootUrl);
            String classname = "com.github.xch168.network.NetworkClassLoaderTest";
            Class clazz = networkClassLoader.loadClass(classname);
            System.out.println(clazz.getClassLoader());
            Object obj = clazz.newInstance();
            Method method = clazz.getDeclaredMethod("sayHello");
            method.invoke(obj, null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Step5. 执行ClassLoaderTest的main方法

运行结果:

com.github.xch168.classloadertest.NetworkClassLoader@214c265e
Hello from network class.

参考链接

  1. https://blog.csdn.net/itachi85/article/details/78088701
  2. https://blog.csdn.net/briblue/article/details/54973413
  3. https://blog.csdn.net/xyang81/article/details/7292380

本文分享自微信公众号 - 编码前线(gh_acef1225aadd)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-01-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 高效地加载Bitmap

    一张2048x1536像素的图片,采用ARGB_8888进行存储,那么内存大小2048 x 1536 x 4 = 12M,如果inSampleSize = 4,...

    用户1205080
  • 详细介绍Java虚拟机(JVM)

      一个运行中的Java虚拟机有着一个清晰的任务:执行Java程序。程序开始执行时他才运行,程序结束时他就停止。你在同一台机器上运行三个程序,就会有三个运行中的...

    用户1205080
  • IntentService解析

    Step1. 定义IntentService的子类:传入线程名称、重写 onHandleIntent()方法

    用户1205080
  • 虚拟机类加载机制(3)——线程上下文类加载器

    之所以将线程上下文类加载器(Thread Context ClassLoader)单独拿出来写,确实是因为它涉及的东西比较多,既然带有线程两个字,一定也是非常重...

    用户1148394
  • Java魔法堂:类加载器入了个门

    一、前言                               《Java魔法堂:类加载机制入了个门》中提及整个类加载流程中只有加载阶段作为码农的我们可以...

    ^_^肥仔John
  • JAVA之ClassLoader

    JAVA基础系列之ClassLoader 一,Java类的加载、链接与初始化 1,加载:查找并加载类的二进制数据 • 通过一个类的全限定名来获取定义此类的二进制...

    Spark学习技巧
  • JVM学习记录-类加载器

    JVM设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外面去实现,以便让应用程序自己决定如何去获取所需要的...

    纪莫
  • 理解类装载器

    类装载器是 Java 中的一项创新,它使得 Java 虚拟机可以在执行的过程中再把一个 Java 类读入虚拟机,提高了程序的灵活性。在Java中,类的信息是被保...

    java乐园
  • iOS客户端启动速度优化实践

    应用启动时间,直接影响用户对一款应用的判断和使用体验。头条主app本身就包含非常多并且复杂度高的业务模块(如新闻、视频等),也接入了很多第三方的插件,这势必会拖...

    ios-lan
  • JVM中的类加载器

      把类加载阶段中的"通过一个类的全限定名来获取描述此类的二进制字节流"这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类,实现这...

    用户4919348

扫码关注云+社区

领取腾讯云代金券