Java 类加载机制详解

 

好像加载机制是 Java 语言的一律好亮点,使得 Java 类可以给动态加载到 Java
虚拟机中。

这次我们扔开术语和概念,从例子入手,由浅入好地讲学 Java 的好像加载机制。

本文涉及知识点:双亲委托机制、 class=”wp_keywordlink”>BootstrapClassLoader、ExtClassLoader、AppClassLoader、自定义网络接近加载器等

章提到代码:
https://github.com/wingjay/HelloJava/blob/master/common/src/classloader/HelloClassLoader.java

什么是 Java 类加载机制?

Java 虚拟机一般采用 Java 类的流水线也:首先以开发者编写的 Java
源代码(.java文件)编译成 Java
字节码(.class文件),然后类加载器会读取这个 .class 文件,并更换成为
java.lang.Class 的实例。有矣拖欠 Class 实例后,Java 虚拟机可以利用
newInstance 之类的措施创建其确对象了。

ClassLoader 是 Java 提供的好像加载器,绝大多数的切近加载器都连续自
ClassLoader,它们叫用来加载不同来的 Class 文件。

Class 文件来哪些来源呢?

上文提到了 ClassLoader 可以错过加载多种自的
Class,那么具体有什么来源为?

第一,最常见的是开发者在应用程序中修的类,这些类似位居项目目录下;

下一场,有 Java
内部自带的核心类如 java.langjava.mathjava.io 等 package
内部的近乎,位于 $JAVA_HOME/jre/lib/ 目录下,如 java.lang.String 类就是概念在 $JAVA_HOME/jre/lib/rt.jar 文件里;

另外,还有
Java 核心扩展类,位于 $JAVA_HOME/jre/lib/ext 目录下。开发者也可拿温馨编排的切近打包改成
jar 文件放入该目录下;

末还有一样栽,是动态加载远程的 .class 文件。

既是有诸如此类多花色的来,那么在 Java 里,是由有一个现实的 ClassLoader
来归并加载也?还是由于多只 ClassLoader 来协作加载呢?

哪些 ClassLoader 负责加载上面几乎类似 Class?

实际上,针对地方四种植来自之近乎,分别产生两样的加载器负责加载。

第一,我们来拘禁级别最高的 Java 核心类,即$JAVA_HOME/jre/lib 里的骨干
jar 文件。这些看似是 Java
运行的功底类,由一个叫作也 BootstrapClassLoader 加载器负责加载,它为深受称作 根加载器/引导加载器。注意,BootstrapClassLoader 比较新鲜,它不就承 ClassLoader,而是由于
JVM 内部贯彻;

然后,需要加载 Java 核心扩展类,即 $JAVA_HOME/jre/lib/ext 目录下的
jar
文件。这些文件由 ExtensionClassLoader 负责加载,它也于称作 扩展类加载器。当然,用户一旦把好出的
jar 文件在这目录,也会于 ExtClassLoader 加载;

连片下去是开发者在项目被修的接近,这些文件拿由 AppClassLoader 加载器进行加载,它呢吃称作 系统类加载器 System ClassLoader

最后,如果想远程加载如(本地文件/网络下载)的计,则要要团结打定义一个
ClassLoader,复写其中的 findClass() 方法才能够得实现。

之所以会望,Java 里提供了起码四类 ClassLoader 来分别加载不同来之
Class。

这就是说,这几种植 ClassLoader 是安合作来加载一个看似为?

这些 ClassLoader 以何种方式来协作加载 String 类呢?

String 类是 Java 自带的极常用的一个接近,现在的题材是,JVM 将因为何种措施将
String class 加载进来吧?

咱来怀疑下。

首先,String 类属于 Java
核心类,位于 $JAVA_HOME/jre/lib 目录下。有的朋友会马上反应过来,上文中领到过了,该目录下之类会由 BootstrapClassLoader 进行加载。没错,它真的是出于 BootstrapClassLoader 进行加载。但,这种对的前提是若曾掌握了
String 在 $JAVA_HOME/jre/lib 目录下。

那么,如果您并不知道 String
类究竟在哪吧?或者自己期待而错过加载一个 unknown 的类呢?

部分朋友这时会说,那非常简短,只要去遍历一全勤所有的类似,看看这个 unknown 的类位于何,然后重新就此相应之加载器去加载。

是的,思路特别对。那该怎么样去遍历呢?

按部就班,可以事先遍历用户自己写的类,如果找到了不畏用 AppClassLoader 去加载;否则去遍历
Java 核心类目录,找到了就是用 BootstrapClassLoader 去加载,否则便错过遍历
Java 扩展类库,依次类推。

这种思路方向是科学的,不过是一个漏洞。

若开发者自己伪造了一个 java.lang.String 类,即当品种中开创一个确保java.lang,包内创建一个称吧 String 的好像,这了可以形成。那要下点的遍历方法,是不是这个类别中之所以到之
String
不是都变成了此伪造的 java.lang.String 类吗?如何缓解者问题也?

化解办法很粗略,当找一个类似时,优先遍历最高级别的 Java
核心类,然后再次夺遍历 Java
核心扩展类,最后重复遍历用户从定义类,而且这个遍历过程是如果找到就及时终止遍历。

当 Java
中,这种实现方式吧如作 双亲委托。其实特别简单,把 BootstrapClassLoader 想象吧中心高层领导人, ExtClassLoader 想象为中层干部, AppClassLoader 想象吧一般公务员。每次要加载一个看似,先获一个体系加载器 AppClassLoader 的实例(ClassLoader.getSystemClassLoader()),然后朝上面层层请求,由最上级优先去加载,如果上面觉得这些类似非属于核心类,就可放至各个子级负责人去自动加载。

一般来说图所示:

图片 1

诚然是按部就班双亲委托计展开类似加载吗?

下通过几只例子来证实点的加载方式。

开发者自定义之类会被 AppClassLoader 加载吗?

于列面临开创一个叫吧 MusicPlayer 的好像公事,内容如下:

package classloader;

public class MusicPlayer {
    public void print() {
        System.out.printf("Hi I'm MusicPlayer");
    }
}

下一场来加以载 MusicPlayer

private static void loadClass() throws ClassNotFoundException {
    Class<?> clazz = Class.forName("classloader.MusicPlayer");
    ClassLoader classLoader = clazz.getClassLoader();
    System.out.printf("ClassLoader is %s", classLoader.getClass().getSimpleName());
}

打印结果也:

ClassLoader is AppClassLoader

可以证明,MusicPlayer 是由 AppClassLoader 进行的加载。

验证 AppClassLoader 的家长真的是 ExtClassLoader 和 BootstrapClassLoader 吗?

此时发现 AppClassLoader 提供了一个 getParent() 的方法,来打印看看都是啊。

private static void printParent() throws ClassNotFoundException {
        Class<?> clazz = Class.forName("classloader.MusicPlayer");
        ClassLoader classLoader = clazz.getClassLoader();
        System.out.printf("currentClassLoader is %s\n", classLoader.getClass().getSimpleName());

        while (classLoader.getParent() != null) {
            classLoader = classLoader.getParent();
            System.out.printf("Parent is %s\n", classLoader.getClass().getSimpleName());
        }
}

打印结果为:

currentClassLoader is AppClassLoader
Parent is ExtClassLoader

第一会见到 ExtClassLoader 确实是 AppClassLoader 的爹娘,不过也不曾看出 BootstrapClassLoader。事实上,上文就领取了, BootstrapClassLoader比较突出,它是由
JVM 内部贯彻之,所以 ExtClassLoader.getParent() = null

如果把 MusicPlayer 类挪到 $JAVA_HOME/jre/lib/ext 目录下会发生什么?

上文中说了,ExtClassLoader 会加载$JAVA_HOME/jre/lib/ext 目录下所有的
jar
文件。那来尝试下直接把 MusicPlayer 这个类放到 $JAVA_HOME/jre/lib/ext 目录下吧。

下下发号施令可以拿 MusicPlayer.java 编译打包改成 jar
文件,并放置到相应目录。

javac classloader/MusicPlayer.java
jar cvf MusicPlayer.jar classloader/MusicPlayer.class
mv MusicPlayer.jar $JAVA_HOME/jre/lib/ext/

这时候 MusicPlayer.jar
已经深受停与 $JAVA_HOME/jre/lib/ext 目录下,同时将前的 MusicPlayer 删除,而且就同一不成刻意使用 AppClassLoader 来加载:

private static void loadClass() throws ClassNotFoundException {
    ClassLoader appClassLoader = ClassLoader.getSystemClassLoader(); // AppClassLoader
    Class<?> clazz = appClassLoader.loadClass("classloader.MusicPlayer");
    ClassLoader classLoader = clazz.getClassLoader();
    System.out.printf("ClassLoader is %s", classLoader.getClass().getSimpleName());
}

打印结果吧:

ClassLoader is ExtClassLoader

证实就是直接用 AppClassLoader 去加载,它还会为 ExtClassLoader 加载到。

从今源码角度真的清楚双亲委托加载机制

上面已经过有例了解了双亲委托的有特色了,下面来拘禁一下它的实现代码,加深理解。

打开 ClassLoader 里的 loadClass() 方法,便是急需分析的源码了。这个艺术里举行了下几乎起事:

  1. 检查对象class是否就加载了,如果加载了则直回到;
  2. 倘没加载了,把加载请求传递给 parent 加载器去加载;
  3. 一旦 parent 加载器加载成功,则直接回到;
  4. 若果 parent 未加载到,则自己调用 findClass()
    方法进行搜,并把寻找结果回到。

代码如下:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // 1. 检查是否曾加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 优先让 parent 加载器去加载
                    c = parent.loadClass(name, false);
                } else {
                    // 如无 parent,表示当前是 BootstrapClassLoader,调用 native 方法去 JVM 加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // 如果 parent 均没有加载到目标class,调用自身的 findClass() 方法去搜索
                long t1 = System.nanoTime();
                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;
    }
}

// BootstrapClassLoader 会调用 native 方法去 JVM 加载
private native Class<?> findBootstrapClass(String name);

圈罢实现源码相信会产生还完整的知道。

类似加载器最要命的另一方面:自定义类加载器

眼前提到了 Java
自带的加载器 BootstrapClassLoaderAppClassLoaderExtClassLoader,这些都是
Java 已经提供好的。

要是真正有意思的,是 自定义类加载器,它同意我们于运行时可以从本地磁盘或网络上动态加载自定义类。这使得开发者可以动态修复某些有题目的接近,热更新代码。

下来贯彻一个网络类加载器,这个加载器可以起网络直达动态下载 .class
文件并加载到虚拟机中使用。

后面我还会见做与 热修复/动态更新 相关的篇章,这里先上 Java
层 NetworkClassLoader 相关的法则。

  1. 作为一个 NetworkClassLoader,它首先使继承 ClassLoader
  2. 下一场它如果兑现ClassLoader内的 findClass() 方法。注意,不是loadClass()方法,因为ClassLoader提供了loadClass()(如上面的源码),它见面因双亲委托建制去找寻某个
    class,直到搜索未顶才见面调用自身之findClass(),如果直白复写loadClass(),那还要实现双亲委托机制;
  3. 在 findClass() 方法里,要由网上下载一个 .class 文件,然后转向成
    Class 对象供虚拟机使用。

现实贯彻代码如下:

/**
 * Load class from network
 */
public class NetworkClassLoader extends ClassLoader {

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = downloadClassData(name); // 从远程下载
        if (classData == null) {
            super.findClass(name); // 未找到,抛异常
        } else {
            return defineClass(name, classData, 0, classData.length); // convert class byte data to Class<?> object
        }
        return null;
    }

    private byte[] downloadClassData(String name) {
        // 从 localhost 下载 .class 文件
        String path = "http://localhost" + File.separatorChar + "java" + File.separatorChar + name.replace('.', File.separatorChar) + ".class"; 

        try {
            URL url = new URL(path);
            InputStream ins = url.openStream();
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead); // 把下载的二进制数据存入 ByteArrayOutputStream
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    public String getName() {
        System.out.printf("Real NetworkClassLoader\n");
        return "networkClassLoader";
    }
}

本条仿佛的意图是自从网及(这里是自个儿的 local apache
服务器 http://localhost/java 上)目录里去下载对应的 .class
文件,并转移成 Class<?> 返回回去用。

下我们来利用这 NetworkClassLoader 去加载 localhost
上的 MusicPlayer 类:

  1. 首先把 MusicPlayer.class 放置于 /Library/WebServer/Documents/java (MacOS)目录下,由于
    MacOS 自带 apache 服务器,这里是服务器的默认目录;
  2. 行下一段代码:

    String className = "classloader.NetworkClass";
    NetworkClassLoader networkClassLoader = new NetworkClassLoader();
    Class<?> clazz  = networkClassLoader.loadClass(className);
    
  3. 正常运转,加载 http://localhost/java/classloader/MusicPlayer.class成功。

得视 NetworkClassLoader 可以正常干活,如果读者要用的言语,只要稍微修改
url 的拼凑方式即可自行下。

小结

好像加载方式是 Java
上生创新的如出一辙起技术,给未来的热修复技术提供了恐。本文力求通过简单的言语及适度的例证来讲学中双亲委托机制自定义加载器等,并出了自定义之NetworkClassLoader

本,类加载是颇风趣的技能,很为难掩所有知识点,比如不同类加载器加载与一个近似,得到的实例却休是和一个等等。

然后我还会见写有关热修复/动态更新相关的技艺,欢迎关注。谢谢。

相关文章