ClassLoader和双亲委派机制

Java基础

浏览数:17

2020-5-30

AD:资源代下载服务

前言:网上关于类加载器讲解的文章特别多,在学习的时候让我受益匪浅,某段时间觉得自己懂了。但是在昨天遇到一个问题,并去看Spark关于用户类加载的时候,它实现的类加载器让我看的很疑惑,半天没有转过来。我才发现自己原来根本不懂类加载器的原理,对双亲委派机制只是停留在字面上,知道会先找父但是不知道怎么去找的,所以今天把JDK关于ClassLoader的代码撸了一遍,把以前一些模糊的地方捋明白了,内心稍安。同时这也是我昨天遇到的问题的前篇,扫清后面问题的障碍,后续会把关于Spark的问题捋出来,再来分享

三种缺省类加载器

当一个JVM启动的时候,Java默认有三种类加载器

  • 启动(Bootstrap)类加载器:Bootstrap类加载器是由C和C++实现的类加载器,它负责将 <Java_Runtime_Home>/lib 或者由 -Xbootclasspath 指定位置下的类库加载到内存中。由于是native的,所以我们无法直接接触它,也无法直接引用它。在JDK的ClassLoader类里可以看到关于它的方法调用都是private native
  • 扩展(Exttension)类加载器:ExtClassLoader是由Sun公司提供的实现,它负责将 < Java_Runtime_Home >/lib/ext 或者由系统变量 java.ext.dir 指定位置中的类库加载到内存中,在 sun.misc.Launcher$ExtClassLoader 可看到源码,由于它的访问权限是default,所以只能是包内可见,我们外部无法直接引用它
  • 应用(Application)类加载器(也称系统类加载器SystemClassLoader):AppClassLoader也是由Sun公司提供的实现,它负责将系统类路径(CLASSPATH)中指定的类库加载到内存中,在 sun.misc.Launcher$AppClassLoader 可看到源码,它的访问权限同样是default,所以只能是包内可见,我们外部无法直接引用它

双亲委派机制

通俗的讲,就是某个特定的类加载器在接到加载类的请求时,它首先会查看自己已加载的类中是否有这个类,如果有就返回,如果没有就将加载任务委托给父类(parent)加载器加载,依次递归,如果父类加载器可以完成类加载任务,就成功返回,只有当父类加载器无法完成此加载任务时,才自己去加载

双亲委派机制下三种类加载器的关系(图片来自网络):

ClassLoaderA和ClassLoaderB是我们自己实现的类加载器,这里都指定了其父加载器为APPClassLoader,当然你也可以指定ClassLoaderA的父加载器为ClassLoaderB,但由于往上最多只能拿到SystemClassLoader的引用,所以父加载器最多只能指定到SystemClassLoader

通过 java.lang.ClassLoader.getSystemClassLoader 我们可以获取到JVM启动时加载ClassPath里jar包的应用类加载器SystemClassLoader,由此我们可以从代码上看到上面的关系图:

这里可以看到扩展类加载器的parent为null,并不是Bootstrap类加载器,那双亲委派到这一级是如何实现的呢? 其次应用类加载器的父加载器为什么是扩展类加载器呢?

双亲委派机制的实现

关系图里父加载器的来源

扩展类加载器和应用类加载器都是由JVM创建的,通过 sun.misc.Launcher 类的构造函数我们可以看到创建过程(具体看注释),如下:

public Launcher() {
  Launcher.ExtClassLoader var1;
  try {
    // 先创建扩展类加载器,加载<Java_Runtime_Home>/lib/ext下的jar包,扩展类加载器的构造方法默认指定的父类加载器就是null,因为我们引用不到BootstrapClassLoader
    var1 = Launcher.ExtClassLoader.getExtClassLoader();
  } catch (IOException var10) {
    throw new InternalError("Could not create extension class loader");
  }

  try {
    // 接着创建应用类加载器,并指定其父加载器为上面创建的ExtClassLoader,由此其父加载器为ExtClassLoader的关系成立
    this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
  } catch (IOException var9) {
    throw new InternalError("Could not create application class loader");
  }
  // 并把当前现场的类加载器设置为应用类加载器
  Thread.currentThread().setContextClassLoader(this.loader);
  
  // ... other code 
}

由于Launcher类里有自己的静态实例,所以该类被BootstrapClassLoader加载的时候就执行了上述过程,创建了扩展类加载器和应用类加载器,并且指定了父加载器关系

双亲委派的机制

除启动类加载器是native实现的外,其他所有类加载器都是继承自 java.lang.ClassLoader 抽象类,该类有一个protected的loadClass方法,双亲委派机制就在该方法中

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();
            try {
                // 检查父加载器
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 使用Bootstrap类加载器加载,所以当ExtClassLoader的parent为null时,它会请求Bootstrap类加载器加载,这样双亲委派机制就是成立的
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                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;
    }
}

其中 findBootstrapClassOrNull 方法调用了 nativefindBootstrapClass 方法,所以是映射到了Bootstrap类加载器的

再来看看扩展类加载器和应用类加载器的类图

可以发现它们都继承自URLClassLoader类,而URLClassLoader类又继承自SecureClassLoader类,在这两个父类中都没有对ClassLoader抽象类的 loadClass 方法重写,而除了AppClassLoader类对 loadClass 方法做了简单包装之外,都没有去更改ClassLoader抽象类的 loadClass 方法原始逻辑,所以 这几个类加载器都保留了双亲委派机制,而如果要改变这种机制,我们可以通过重写这个方法实现

自己实现ClassLoader

实现一个ClassLoader比较简单,我们像扩展类加载器和应用类加载器一样继承 URLClassLoader 就可以实现我们自己的类加载器,并利用它加载包和我们的class

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class MyClassLoaderTest {
  static class MyClassLoader extends URLClassLoader {
    public MyClassLoader(URL[] urls, ClassLoader parent) {
      super(urls, parent);
    }

    // 对外暴露addURL方法
    @Override
    public void addURL(URL url) {
      super.addURL(url);
    }
  }
  
  public static void main(String[] args) throws Exception {
    String path = "D:\\maven\\repository\\commons-codec\\commons-codec\\1.9\\commons-codec-1.9.jar";

    // 也可用 ClassLoader.getSystemClassLoader() 获取应用类加载器作为父加载器或者直接置父加载器为null
    MyClassLoader myClassLoader = new MyClassLoader(new URL[0], Thread.currentThread().getContextClassLoader());
    myClassLoader.addURL(new File(path).toURI().toURL());

    // 加载包里的类
    Class<?> clazz = myClassLoader.loadClass("org.apache.commons.codec.digest.DigestUtils");
    Method md5Hex = clazz.getDeclaredMethod("md5Hex", String.class);
    // 执行MD5
    String result = (String) md5Hex.invoke(null, "my classloader test");
    // 判断结果
    assert result.equals("53c6c4f7ceadadcf36ce1386678fb61b");
    // 设置当前线程的类加载器为我自己的类加载器
    // Thread.currentThread().setContextClassLoader(myClassLoader);
  }
}

欢迎阅读转载,转载请注明出处:https://my.oschina.net/kavn/blog/1579576

作者:问津已非少年