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
来统Samsung载呢?仍旧由八个 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

理所当然,类加载是很风趣的技艺,很难覆盖所有知识点,比如不相同类加载器加载同一个类,拿到的实例却不是同一个等等。

尔后我还会撰写有关热修复/动态更新相关的技巧,欢迎关心。多谢。

相关文章