1

从源码角度理解类加载器

 3 years ago
source link: https://www.techstack.tech/post/%E4%BB%8E%E6%BA%90%E7%A0%81%E8%A7%92%E5%BA%A6%E7%90%86%E8%A7%A3%E7%B1%BB%E5%8A%A0%E8%BD%BD%E5%99%A8/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

虚拟机类加载机制就是把 Class 文件加载到内存,并对数据进行校验、解析和初始化,最终形成可以被虚拟机直接使用的 Java 类型。一个类有虚拟机加载到内存中的流程包含了多个阶段,其中实现加载阶段中的通过一个类的全限定名来获取描述此类的二进制字节流的动作便是有类加载器来完成的。

一般来说、类的加载流程都有虚拟自主完成,开发人员不必关系其实现细节。但是如过如果遇到了需要与类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就很容易花大量的时间去调试 ClassNotFoundExceptionNoClassDefFoundError 等异常。同时随着类加载器在类层次划分、OSGi、热部署、代码加密等领域大放异彩,对于开发人员还是很有必要去掌握其中的原理的。

类加载运行的基本流程

package tech.stack;

/**
 * @author jianyuan.chen
 * @date 2020/8/20 15:51
 */
public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello world");
    }
}

对于一个 Java 程序的运行过程从整体上说可以分为两个步骤:

  1. 使用javac 命令将HelloWorld.java源文件编译成HelloWorld.class文件。
  2. java 命令运行HelloWorld.class文件。

在运行 java 命令时,具体发生了什么,可以通过下图总结一下:

类加载流程
类加载流程

类加载器分类与结构

从上图能看出 Java 程序的入口为sun.misc.Launcher类,查看一下Launcher类的方法

public class Launcher {    
      private static Launcher launcher = new Launcher();
        private ClassLoader loader;
        ...
        public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        ....
    }

        public ClassLoader getClassLoader() {
        return this.loader;
    }
  ...
    static class ExtClassLoader extends URLClassLoader {
        private static volatile Launcher.ExtClassLoader instance;

        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            if (instance == null) {
                Class var0 = Launcher.ExtClassLoader.class;
                synchronized(Launcher.ExtClassLoader.class) {
                    if (instance == null) {
                        instance = createExtClassLoader();
                    }
                }
            }

            return instance;
        }
      ...
        }        
        ...
        static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
      ...
        } 
  ...
}

从 Launcher 入口可以看出在类加载过程中使用到ExtClassLoaderAppClassLoader两个系统类加载器,加上启动 Launcher 类使用的BootStrapClassLoader一共有三个系统类加载器。

思维扩展:

Launcher.getLauncher()使用了饿汉单例模式创建。

ExtClassLoader.getExtClassLoader() 使用了懒汉双重校验单例模式。

BootStrapClassLoader

引导类加载器,通过 C++ 代码实现,不属于 Java 程序,负责加载支撑 JVM 运行的位于 JAVA_HOME/lib目录中或者被-Xbootclasspath参数所指定的路径中的类库,比如:rt.jar、charset.jar 等。引导类加载器的加载路径可以通过System.getProperty("sun.boot.class.path");

ExtClassLoader

扩展类加载器负责加载 JAVA_HOME/ext目录中或者被java.ext.dir系统变脸所指定的路径中的所有类库,扩展类加载器可以被直接使用。扩展类加载器的加载路径可以通过System.getProperty("java.ext.dirs");获取。

AppClassLoader

应用程序类加载器,由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值。所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上指定的类库,这个类加载器也是可以被开发者直接使用的,如果应用程序中没有定义过自己的类加载器一般情况下这个就是程序中的默认类加载器。应用程序类加载器的加载路径可以通过System.getProperty("java.class.path")获取。

ExtClassLoad 和 AppClassLoader 都属于 Launcher 的静态内部类,同时也都是 ClassLoader 的子类。Launcher 在实例化的过程中,创建了 ExtClassLoader 和 AppClassLoader。getClassLoader()返回的是一个 AppClassLoader 的实例。通过Idea查看整个 ClassLoader 的体系结构如下,虚拟机启动只使用了 ExtClassLoader 和 AppClassLoader。

AppClassLoader
AppClassLoader

通过上文所述,运行一下代码:

public class ClassLoaderPathTest {
    public static void main(String[] args) {
        Arrays.stream(System.getProperty("sun.boot.class.path").split(":")).forEach(System.out::println);
        System.out.println("------------------");
        Arrays.stream(System.getProperty("java.ext.dirs").split(":")).forEach(System.out::println);
        System.out.println("------------------");
        Arrays.stream(System.getProperty("java.class.path").split(":")).forEach(System.out::println);
    }
}

/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/resources.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/sunrsasign.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jsse.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jce.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfr.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/classes
------------------
/Users/chenjianyuan/Library/Java/Extensions
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext
/Library/Java/Extensions
/Network/Library/Java/Extensions
/System/Library/Java/Extensions
/usr/lib/java
------------------
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/charsets.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/deploy.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/cldrdata.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/dnsns.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jaccess.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/jfxrt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/localedata.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/nashorn.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunec.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunjce_provider.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/sunpkcs11.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/ext/zipfs.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/javaws.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jce.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfr.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jfxswt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/jsse.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/management-agent.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/plugin.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/resources.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/jre/lib/rt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/ant-javafx.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/dt.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/javafx-mx.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/jconsole.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/packager.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/sa-jdi.jar
/Library/Java/JavaVirtualMachines/jdk1.8.0_231.jdk/Contents/Home/lib/tools.jar
/Users/chenjianyuan/IdeaProjects/course/target/classes
/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar

观察上面输出内容发现一些本应该在 BootstrapClassLoader 和 ExtClassLoader 类加载器中加载的 jar 包也存在了 AppClassLoader 类加载器加载的路径下。这会不会造成类的重复加载呢,答案是不会的,这就涉及到了类加载器的双亲委派加载模型。

JVM类加载机制

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入

  • 父类委托:先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类

  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效

双亲委派模型(Parents Delegation Model)

何为双亲委派

双亲委派模型
双亲委派模型

上图所示类加载器之间的层次关系就成为类加载器的双亲委派模型。

类加载器之间结构和关系

在深入理解双亲委派模型之前先运行一段代码:

public class ClassLoaderTest {
    public static void main(String[] args) {
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println("system classLoader is " + systemClassLoader);
        ClassLoader systemClassLoaderParent = systemClassLoader.getParent();
        System.out.println("system classLoader parent is " + systemClassLoaderParent);
        ClassLoader systemClassLoaderParentParent = systemClassLoaderParent.getParent();
        System.out.println("system classLoader parent's parent is " + systemClassLoaderParentParent);
    }
}
==============
system classLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
system classLoader parent is sun.misc.Launcher$ExtClassLoader@61bbe9ba
system classLoader parent's parent is null

从上面的代码可以看出类加载之间的关系,AppClassLoader 为系统类加载器,上层加载器为 ExtClassLoader, ExtClassLoader 上层类加载器为 null 即为 BootstrapClassLoader,此处也说明如果我们想让自定义加载的上层类加载器为 BootstrapClassLoader 类加载器的话,可以直接将其 parent 属性置为 null

值得注意的是此处的 parent 不能理解为父类加载器,类加载器之间的关系不是通过继承来实现的,而都是使用组合关系来复用上层类加载器的代码。可以通过 AppClassLoader、ExtClassLoader 的 UML 类图查看一下之间的关系。

ClassLoader
ClassLoader

AppClassLoader 和 ExtClassLoader 都继承了 URLClassLoader 最终继承了 ClassLoader 同时又依赖 ClassLoader即 AppClassLoader 依赖 ExtClassLoader。

双亲委派模型工作过程

自定义类加载器在收到一个类加载请求是,它不会自己首先去加载这个类,而是委托给上层类加载器也就是应用程序类加载器去加载,同样在应用程序类加载器收到类加载的请求时也不会去首先加载这个类,而是委托上层即 ExtClassLoader 类加载器去加载,这样一直到委托到顶层 BootstrapClassLoader,在 BootstrapClassLoader 接收到类加载请求后,会在自己的搜索范围内搜索这个类,如果搜索不到则返回至下层类加载器,让下层类加载器去自己的搜索范围内去搜索此类,直至最底层类加载器。如果所有的加载器都没有搜索到此类则会抛出 ClassNotFoundException

双亲委派模型源码解析

在了解双亲委派模型运行机制之前要先熟悉 ClassLoader 类加载器的结构,其核心代码如下面所示:

public abstract class ClassLoader {
      // 上层类加载器
    private final ClassLoader parent;

    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        ...
    }
      // 构造方法 初始化上层类加载器
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
      // 默认上层类加载器为系统类加载器
      protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    // 加载类方法 核心
    protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // step1. 首先检查类是否已经被加载过
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    // 被加载类如未被加载过,并且其上层类加载器不为空,委托上层类加载器进行加载。
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                      // 被加载类没有被加载过,并且此加载此类的类加载器没有上层类加载器,由 BootstapClassLoader 进行加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) { // 此处的 ClassNotFoundException 为下面 findClass() 方法抛出,此处 catch 的为上层类加载器抛出的异常
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
                          // 从最底层类加载器一直委托到 BootStrapClassLoader 如果 BootStrapClassLoader 类加载器没有加载此类为空,
                // 由 BootStrapClassLoader 类加载器下层寻找,找不到此类抛出 ClassNotFoundException 并由 
                // BootStrapClassLoader 下层的下层捕获异常,一直到最底层找不到则程序抛出 ClassNotFoundException
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    // 根据类的全限定名在当前类加载器搜索范围内查询需要被加载的类,如果找不到则
                             // 抛出 ClassNotFoundException
                    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;
        }
    }
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

   protected final Class<?> findLoadedClass(String name) {
        if (!checkName(name))
            return null;
        return findLoadedClass0(name);
    }

   private native final Class<?> findLoadedClass0(String name);

}

ClassLoader 类加载器整体结构如上所示,其加载类的核心方法为 loadClass 和 findClass,findClass 由其子类 URLClassLoader 重载实现。并由 URLClassLoader 子类 AppClassLoader 和 ExtClassLoader 继承获得实现。

Launcher
Launcher

结合源码对 Hello World 程序运行进行分析:

  1. BootStrapClassLoader 在启动加载Launcher,并通过 getLauncher 获取 Launcher ①

  2. Launcher 在 new 时会调用 ExtClassLoader.getExtClassLoader(②)创建 ExtClassLoader(③),查看 ExtClassLoader 创建方法

                 public ExtClassLoader(File[] var1) throws IOException {
                super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
                SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
            }

    可以看出 ExtClassLoader 初始化其 parent 属性为 null,根据 ClassLoader的 loadClass 方法即可知 ExtClassLoadClass 类加载器会委托 BootstrapClassLoader 类加载器进行类加载。在 ExtClassLoader 类加载器创建创完成后,会创建 AppClassLoader, 并且把已经创建的 ExtClassLoader 类加载赋值给 AppClassLoader 类加载器的 parent 属性(④)。最后由 AppClassLoader 创建实例并返回(⑤)赋值给 Luancher 类的 loader 属性(④)。

  3. 由 loader(AppClassLoader) 调用 loaderClass 方法加载tech.stack.HelloWorld

    1. 从 LoadedClass 缓存中查询 tech.stack.HelloWorld 类,此处为 Native 方法由虚拟机底层实现。
    2. 此处为tech.stack.HelloWorld首次加载,并不会在 loadedClass 缓存中查到 Class对象
    3. 判断 AppClassLoader 的 parent 是否为空,此处AppClassLoader.parent 为 extClassLoader加载器。
    4. 委托由 extClassLoader 类加载器加载 tech.stack.HelloWorld 类。
    5. extClassLoader 同样进行 3.1、3.2步骤。
    6. 判断 extClassLoader 的 parent 对象是否为空,由 2 步骤可知 extClassLoader.parent 为 null。
    7. 委托由 BootstrapClassLoader 尝试加载tech.stack.HelloWorld,由于 BootstrapClassLoader 扫面范围内不存在tech.stack.HelloWorld类,因此返回 null。
    8. Class对象为 null,则由 extClassLoader 通过在 URLClassLoader 类加载器继承到的 findClass 方法在 extClassLoader 搜索范围内查询tech.stack.HelloWorld类。
    9. 在 extClassLoader 搜索范围内找不到tech.stack.HelloWorld类,抛出 ClassNotFoundException
    10. ClasseException由 appClassLoader 捕获处理,然后在 AppClassLoader 搜索范围内查询tech.stack.HelloWorld类。
    11. 在 appClassLoader 搜索范围内可以找到tech.stack.HelloWorld类,将 Class对象返回,并判断是否需要解析。
    12. 返回 Class对象。

以上就是tech.stack.HelloWorld 通过双亲委派模型加载的整个流程。

思维扩展:

synchronized (getClassLoadingLock(name)){
  ...
} 
protected Object getClassLoadingLock(String className) {
        Object lock = this;
        if (parallelLockMap != null) {
            Object newLock = new Object();
            lock = parallelLockMap.putIfAbsent(className, newLock);
            if (lock == null) {
                lock = newLock;
            }
        }
        return lock;
}

synchronized 保证了类加载器在加载类的过程中是线程安全的。保证了类加载过程中初始化阶段只会发生一次。同时在也是单例模式内部静态类这种写法能保证线程安全的基础。

实现双亲委派的代码都集中在 loadClass() 方法之中,总结来说 loadClass 的逻辑就是:先检查类是否已经被加载过,若没有加载则调用上层加载器的 loadClass() 方法,若上层类加载器为空则默认使用启动类加载器。如果上层类加载器加载失败,抛出 ClassNotFoundException异常后,在调用自己的 findClass() 方法进行加载。

为什么设计双亲委派机制

  • 沙箱安全机制:可以防止核心 API 库被随意篡改。例如自己写的 java.lang.String.class 类就不会被加载。
  • 避免类的重复加载:当上层类加载器已经加载了该类时,就没有必要在由下层类加载器加载一遍,保证了类的唯一性。

在程序中编写如下代码:

package java.lang;

public class String {
    public static void main(String[] args) {
        System.out.println("custom String Class");
    }
}
======
错误: 在类 java.lang.String 中找不到 main 方法, 请将 main 方法定义为:
   public static void main(String[] args)
否则 JavaFX 应用程序类必须扩展javafx.application.Application

在项目中新建模块loader-test 并且创建tech.stack.HelloWorld之后将模块打包放入JAVA_HOME/ext目录下面,运行下面代码:

public class App {
    public static void main(String[] args) {
        System.out.println(HelloWorld.class.getClassLoader());
    }
}

上面代码输出sun.misc.Launcher$ExtClassLoader@266474c2说明 HelloWorld类是有 ExtClassLoader 类加载器进行加载的而不是有 AppClassLoader 进行加载。如果继续放到JAVA_HOME/lib目录下面,再次运行上面的代码,得到输出:sun.misc.Launcher$ExtClassLoader@5e481248,结果并没有改变,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载/lib目录下存在的陌生类。换句话说,虚拟机只加载/lib目录下它可以识别的类。因此,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。但是如果在运行时添加jvm 参数-Xbootclasspath/a:/Users/chenjianyuan/IdeaProjects/course/loader-test/target/loader-test.jar 输出的结果则为 null ,就变成了 BootstrapClassLoader 类加载器加载。

Java 程序动态扩展方式

类加载方式有三种:

  • 命令行启动应用时候由JVM初始化加载
  • 通过Class.forName()方法动态加载
  • 通过ClassLoader.loadClass()方法动态加载

Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以加载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。运行时动态扩展java应用程序有如下两个途径:

反射 (调用java.lang.Class.forName(…)加载类)

 public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
    }
 public static Class<?> forName(String name, boolean initialize,
                                   ClassLoader loader)
        throws ClassNotFoundException{

   }

这里的initialize参数是很重要的,它表示在加载同时是否完成初始化的工作(说明:单参数版本的forName方法默认是完成初始化的)。有些场景下需要将initialize设置为true来强制加载同时完成初始化,我们在程序中自己调用方法时一般都会直接初始化,典型的就是加载数据库驱动问题。因为JDBC驱动程序只有被注册后才能被应用程序使用,这就要求驱动程序类必须被初始化,而不单单被加载。

// 加载并实例化JDBC驱动类
Class.forName(driver);

 // JDBC驱动类的实现
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    // 将initialize设置为true来强制加载同时完成初始化,实现驱动注册
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can\'t register driver!");
        }
    }
}    

用户自定义类加载器

通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。

实现自定义类加载器

自定义类加载器只需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(),实现了双亲委派机制,还有一个就是 findClass(),默认实现的是空方法,所以实现自定义类加载器主要是重写 fidClass 方法。

public class MyClassLoader extends ClassLoader {

    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = name.replace(".", "/");
        try {
            FileInputStream fin = new FileInputStream(classPath + "/" + fileName + ".class");
            int available = fin.available();
            byte[] bytes = new byte[available];
            fin.read(bytes);
            fin.close();
            return super.defineClass(name, bytes, 0, bytes.length);
        } catch (Exception ignore) {
            // throw the end
        }
        throw new ClassNotFoundException(name);
    }
}
//  将 tech.stack.HelloWorld.class copy 到 /Users/chenjianyuan/Documents 目录下面
public class App {
    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader("/Users/chenjianyuan/Documents");
        try {
            Class<?> aClass = classLoader.loadClass("tech.stack.HelloWorld");
            System.out.println(aClass);
            System.out.println(aClass.getClassLoader());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

// 输出:
// class tech.stack.HelloWorld
// tech.stack.classload.MyClassLoader@7b3300e5

只要重写 findClass 方法,能获取到可以被虚拟机加载的.class 文件流就可以。因此可以自定义文件流的来源,譬如文件类加载器、网络类加载器、数据库类加载器等等。关于 defineClass() 就是类加载过程中其他验证、准备、解析、初始化等阶段。

打破双亲委派模型

实现自定义加载器打破双亲委派模型

上文分析出 ClassLoader 类加载器的 loadClass() 方法是实现双亲委派模型的关键,因此要实现自定义加载器打破双亲委派模型,重写 loadClass() 方法则为关键的步骤,还是上面的代码,不同的是在项目中添加tech.stack.HelloWorld类型,如下所示:

HelloWorld
HelloWorld

再次运行查看输出日志:

class tech.stack.HelloWorld
sun.misc.Launcher$AppClassLoader@18b4aac2

发现由于双亲委派模型,tech.stack.HelloWorld 是由 AppClassLoader 类加载器进行加载的。修改一下MyClassLoader 代码来打破双亲委派模型:

public class MyClassLoader extends ClassLoader {

    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
              // 此处需要进行判断  非自定义的类比如一些核心的类库(Object)还是需要让上层类加载器去加载
                if (!name.startsWith("tech.stack")) { 
                    c = this.getParent().loadClass(name);
                } else {
                    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;
        }
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        String fileName = name.replace(".", "/");
        try {
            FileInputStream fin = new FileInputStream(classPath + "/" + fileName + ".class");
            int available = fin.available();
            byte[] bytes = new byte[available];
            fin.read(bytes);
            fin.close();
            return super.defineClass(name, bytes, 0, bytes.length);
        } catch (Exception ignore) {
            // throw the end
        }
        throw new ClassNotFoundException(name);
    }
}

public class App {
    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader("/Users/chenjianyuan/Documents");
        try {
            Class<?> aClass = classLoader.loadClass("tech.stack.HelloWorld");
            System.out.println(aClass);
            System.out.println(aClass.getClassLoader());
            Method sayHello = aClass.getMethod("sayHello", null);
            sayHello.invoke(aClass.newInstance(), null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

// 输出:
// class tech.stack.HelloWorld
// tech.stack.classload.MyClassLoader@610455d6
// Hello world!

此处只是简单的一个实现,不同的业务模型实现的模式不同,要根据实际问题具体分析。

判断两个类是否相等的前提是这两个类是同一个类加载器加载的。如下代码:

public class App {
    public static void main(String[] args) {
        MyClassLoader classLoader = new MyClassLoader("/Users/chenjianyuan/Documents");
        try {
            Class<?> aClass = classLoader.loadClass("tech.stack.HelloWorld");
            System.out.println(aClass.newInstance() instanceof HelloWorld);
            Class<?> bClass = ClassLoader.getSystemClassLoader().loadClass("tech.stack.HelloWorld");
            System.out.println(bClass.newInstance() instanceof HelloWorld);
            System.out.println(aClass.equals(bClass));
            System.out.println(bClass.equals(HelloWorld.class));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 输出结果为
// false
// true
// false
// true

这里所指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

为什么要打破双亲委派模型

过双亲委派模型并不是一个强制性的约束模型,而是Java设计者推荐给开发者的类加载器实现方式。虽然双亲委派模型很好的保证了类库的安全和统一,但是一些情况下,这也会成为一些弊端:

  • 在核心底层代码需要调用上层用户代码,典型的便是 JNDI 服务,它的代码由启动类加载器去加载(rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者(SPI,Service Provider Interface)的代码。
  • 程序追求动态性。比如代码热替换(HotSwap)、模块热部署(Hot Deployment)等。
  • 为了保证环境隔离与资源共享。例如 Tomcat隔离多个 webApp jar 包冲突问题、Spring 读取Tomcat 环境中共享资源。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK