Java 类加载机制详解

本文参考网址:https://mp.weixin.qq.com/s/Pmq73a4MAb5BWTsvX2XVcA

何为 Java 类加载机制?

  • Java 虚拟机使用 Java 类的流程为:首先将开发者编写的 Java 源代码(.java文件)编译成 Java 字节码(.class文件),然后类加载器会读取 .class 文件,并转换成 java.lang.Class 的实例。有了该 Class 实例后,Java 虚拟机可以利用 newInstance 之类的方法创建其真正对象了。
  • ClassLoader 是 Java 提供的类加载器,绝大多数的类加载器都继承自 ClassLoader,它们被用来加载不同来源的 Class 文件。
  • public abstract class ClassLoaderextends Object

Class 文件来源?

  • 既然是类加载器(ClassLoader)读取字节码文件(.class文件),那么.class文件有哪些来源呢?
  1. 最常见的是开发者在应用程序中编写的类,这些类位于项目目录下,会由.java文件自动编译成.class文件;
  2. Java 内部自带的核心类如 java.lang、java.math、java.io 等包内部的类,位于 $JAVA_HOME/jre/lib/ 目录下,如 java.lang.String 类就是定义在 $JAVA_HOME/jre/lib/rt.jar 文件里(jar文件就是打包编译好的.class文件集合)
  3. Java 核心扩展类,位于 $JAVA_HOME/jre/lib/ext 目录下,开发者也可以把自己编写的类打包成 jar 文件放入该目录下
  4. 最后一种是动态加载远程的 .class 文件。

对应的类加载器加载对应Class

  • 针对上面四种来源的类文件(.class文件),分别有不同的加载器负责加载。
  1. 级别最高的 Java 核心类,即$JAVA_HOME/jre/lib 里的核心 jar 文件。这些类是 Java 运行的基础类,由一个名为 BootstrapClassLoader 加载器负责加载,它也被称作 根加载器/引导加载器。注意 BootstrapClassLoader 比较特殊,它不继承 ClassLoader,而是由 JVM 内部实现;
  2. Java 核心扩展类,即 $JAVA_HOME/jre/lib/ext 目录下的 jar 文件。这些文件由 ExtensionClassLoader 负责加载,它也被称作 扩展类加载器。用户如果把自己开发的 jar 文件放在这个目录,也会被 ExtClassLoader 加载;
  3. 开发者在项目中编写的类,这些文件将由 AppClassLoader 加载器进行加载,它也被称作 系统类加载器 System ClassLoader;
  4. 如果想远程加载(如本地文件/网络下载)的方式,则必须要自己自定义一个 ClassLoader,复写其中的 findClass() 方法才能得以实现。
  • 因此能看出,Java 里提供了至少四类 ClassLoader 来分别加载不同来源的 Class。

类加载流程

  • 不同的ClassLoader加载不同的类,那么运行时是如何协作来加载一个类呢?
  1. 当查找一个类时,优先BootstrapClassLoader遍历最高级别的 Java 核心类,然后ExtensionClassLoader再去遍历 Java 核心扩展类,最后AppClassLoader 再遍历用户自定义类,而且这个遍历过程是一旦找到就立即停止遍历。在 Java 中,这种实现方式也称作 双亲委托
  2. 可以将 BootstrapClassLoader 想象为核心高层领导人, ExtClassLoader 想象为中层干部, AppClassLoader 想象为普通公务员。每次需要加载一个类,先获取一个系统加载器 AppClassLoader 的实例(ClassLoader.getSystemClassLoader()),然后向上级层层请求,由最上级优先去加载,如果上级觉得这些类不属于核心类,就可以下放到各子级负责人去自行加载。
Java 类加载机制详解

累加器 验证

验证 AppClassLoader

  • 下面可以验证,MusicPlayer 是由 AppClassLoader 进行加载的。
package test.classLoadTest;

/**
 * Created by Administrator on 2018/6/21 0021.
 * 音乐播放器
 */
public class MusicPlayer {
    /**模拟播放音乐*/
    public void palyer() {
        System.out.println("music is player ...");
    }

    public static void main(String[] args) {
        try {
            /**获取Class实例,forName方法参数是类的全路径*/
            Class aClass = Class.forName("test.classLoadTest.MusicPlayer");
            /**获取累加器对象*/
            ClassLoader classLoader = aClass.getClassLoader();
            /**获取累加器的名称
             * 输出:sun.misc.Launcher$AppClassLoader*/
            System.out.println(classLoader.getClass().getName());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

验证双亲委托

  • 可以发现ExtClassLoader 确实是 AppClassLoader 的双亲,不过却没有看到 BootstrapClassLoader。
  • 因为 BootstrapClassLoader 比较特殊,它是由 JVM 内部实现的,所以 ExtClassLoader.getParent() = null。
/**
 * Created by Administrator on 2018/6/21 0021.
 * 音乐播放器
 */
public class MusicPlayer {
    /**模拟播放音乐*/
    public void palyer() {
        System.out.println("music is player ...");
    }

    public static void main(String[] args) {
        try {
            /**获取Class实例,forName方法参数是类的全路径*/
            Class aClass = Class.forName("test.classLoadTest.MusicPlayer");
            /**获取累加器对象*/
            ClassLoader classLoader = aClass.getClassLoader();
            /**获取累加器的名称
             * 输出:class sun.misc.Launcher$ExtClassLoader*/
            ClassLoader extClassLoader = classLoader.getParent();
            System.out.println(extClassLoader.getClass().toString());

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

自定义类加器

  • 自定义类加载器,允许JVM在运行时可以从本地磁盘或网络上动态加载自定义类(.class文件)。这使得开发者可以动态修复某些有问题的类,热更新代码。
  • 下面来实现一个网络类加载器,这个加载器可以从网络上动态下载 .class 文件并加载到虚拟机中使用。
  • 自定义流程如下:
  1. 自定义 NetworkClassLoader 类继承  ClassLoader
  2. 然后实现 ClassLoader 类的 findClass() 方法
  3. ClassLoader 自己提供了 loadClass(),它会基于双亲委托机制去搜索某个 class,当搜索不到时会调用自身的findClass(),如果直接复写loadClass(),那还要实现双亲委托机制;
  4. 在 findClass() 方法里,要从网络上下载一个 .class 文件,然后转化成 Class 对象供虚拟机使用。

class文件准备

package org.xyjt.filter;

/**
 * Created by Administrator on 2018/6/21 0021.
 * 音乐播放器
 */
public class MusicPlayer {
    /**
     * 模拟播放音乐
     */
    public void player() {
        System.out.println("music is player ...");
    }

    /**
     * 音乐直播
     *
     * @param musicName:歌曲名称
     * @return
     */
    public String musicLivePlayer(String musicName) {
        return "music " + musicName + " player is success";
    }
}

Java 类加载机制详解

NetworkClassLoader

package com.lct.utils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;

/**
 * Created by Administrator on 2018/6/21 0021.
 * 自定义网络类加载器---继承ClassLoader
 */
public class NetworkClassLoader extends ClassLoader {
    /**
     * 必须重写findClass方法
     *
     * @param name
     * @return
     * @throws ClassNotFoundException
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        if (name == null || "".equals(name.trim())) {
            return null;
        }
        /**下载网络上的Class文件,返回字节数组*/
        byte[] classData = downloadClassData(name);
        if (classData == null || classData.length <= 0) {
            super.findClass(name);
        } else {
            /**调用ClassLoader重载的defineClass方法
             * 将一个 byte 数组转换为 Class 类的实例。必须分析 Class,然后才能使用它。
             * name:所需要的类的二进制名称,如果不知道此名称,则该参数为 null
             *      classData已经包含了class信息,name设置为null也是可以的
             *      name不为null时,必须是目标类的全路径,如:org.xyjt.filter.MusicPlayer
             */
            return defineClass(null, classData, 0, classData.length);
        }
        return null;
    }

    /**
     * 下载网络上的Class文件
     *
     * @param name .class文件名,也是目标类的名字,如:MusicPlayer
     * @return 返回字节数组
     */
    public byte[] downloadClassData(String name) {
        try {
            /**
             * path拼接完成后如:http://localhost:8080/webProject/javaClass/MusicPlayer.class
             * 这个路径只有确保网络上能访问到此.class文件即可,为了方便立即才写死在这
             * 实际中路径前缀应该放到配置文件中
             */
            String path = "http://localhost:8080/webProject/javaClass/" + name + ".class";
            URL url = new URL(path);
            InputStream inputStream = url.openStream();

            /**用一个字节数组输出流来接收
             * 边读边存,都是常用的IO操作*/
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            int buffSize = 2048;
            byte[] buffer = new byte[buffSize];
            int bytesRead;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                byteArrayOutputStream.write(buffer, 0, bytesRead);
            }
            /**最后返回字节数组*/
            return byteArrayOutputStream.toByteArray();
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    public static void main(String[] args) {
        try {
            /**调用的目标.class文件的名称*/
            String className = "MusicPlayer";

            /**用自定义类加载器类加载此类
             * loadClass方法会使用双亲委托,当找不到时,它默认就是调用上面的findClass方法
             * 当然也可以直接networkClassLoader.findClass("className)*/
            NetworkClassLoader networkClassLoader = new NetworkClassLoader();
            /**aClass就是网络上的动态加载的org.xyjt.filter.MusicPlayer*/
            Class aClass = networkClassLoader.loadClass(className);

            /** 这里肯定输出的是自定义累加器而不是AppClassLoaderExtClassLoaderBootstrapClassLoader*/
            System.out.println("1"+aClass.getClassLoader().getClass().getName());
            System.out.println("2"+aClass.getName());

            /**调用MusicPlayer方法*/
            Method playerMethod = aClass.getMethod("player", null);
            playerMethod.invoke(aClass.newInstance(),null);

            Method musicLivePlayerMethod = aClass.getMethod("musicLivePlayer", String.class);
            Object result = musicLivePlayerMethod.invoke(aClass.newInstance(), "千年等一回");
            System.out.println("3"+result);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}
Java 类加载机制详解