影子的知识库

影子的知识库

  • 知识库
  • GitHub

›Java系列

JVM系列

  • JVM内存区域
  • 对象创建-布局-访问
  • 内存溢出实战
  • 内存区域回收
  • 四大引用
  • 垃圾回收算法
  • HotSpot回收算法细节

Java系列

  • java注解
  • springboot请求参数绑定
  • springboot请求参数校验框架
  • YAML语法
  • 动态代理
  • classpath和java命令
  • springboot-aop编程
  • springboot统一异常处理
  • springboot数据库和事务
  • springboot拦截器
  • springboot中的web配置
  • docker的简单开发
  • springboot自动配置
  • 数据库的隔离级别
  • springboot监控
  • java类加载
  • java-agent的相关内容
  • 类加载器详解
  • java的SecurityManager
  • maven学习

Node

    JS 基础

    • 语法基础和数据类型
    • 数据类型转换
    • 语句 表达式 运算符
    • 变量与对象
    • 函数
    • 数据处理
    • 常用 API
    • 重点知识

    ES6

    • 块级作用域
    • 字符串和正则表达式
    • 函数
    • 对象
    • Symbol
    • Set和Map
    • 迭代器和生成器
    • 类
    • 数组
    • Promise

    Node 基础

    • 模块系统
    • package.json
    • 内置对象
    • npm脚本的使用
    • Buffer
    • Stream
    • 事件循环机制
    • 示例代码

    stream系列

    • 流的缓冲
    • 可读流
    • 可写流
    • 双工流和转换流
    • 自定义流

后期计划

  • 学习计划
  • 专题研究计划
Edit

本文内容

问题

  • Class.getCLassLoader() 获取到的类加载器是什么时候绑定在一起的,有什么语义
  • 我们使用 SomeClass a = new SomeClass() 的时候,SomeClass 这个类是被什么类加载器加载的
  • 两个不同的类加载器加载同一个类,是否会初始化两次
  • Class.forName() 和 classLoader.loadClass() 的区别
  • 默认的线程上下文类加载器是什么
  • 线程上下文类加载器有什么作用
  • 怎么自定义类加载器

说明

本文的源码均来自于我的 Mac 上安装的 adoptopenjdk-8.jdk ,非 Oracle JDK,因此部分细节有可能与 Oracle JDK 8 有所不同,但是应该基本上来说都是一致的。

运行代码时,如果没有明确说明,那么均是使用了 IDEA 提供的 springboot 2.x 模板运行的,主要是为了方便。没有明确说明的时候,都是使用 IDE 的运行按钮去运行的代码,而不是打成 springboot 的 fat jar 运行的。

本文不是很权威,仅仅是说下我个人的理解,因为一直对类加载器这一块儿模模糊糊的,最近正好有时间看这一块儿的内容,决定好好记录一下自己的研究过程和研究成果。

参考

  • classLoader使用与原理分析
  • 深入探讨 java 类加载器
  • 深度分析 java 的 ClassLoader 机制

BootstrapClassLoader

这个类加载器实际上并不存在于 Java 中,它是由 C++ 编写的 JVM 实现的一部分,它的主要作用就是加载 JDK 的核心类库,以 java.* 开头的那些类(可能应该还有一些 sun 开头的类)。

当我们启动 JVM 的时候(视作一个用 C++ 写的普通的进程),BootstrapClassLoader(存在于 C++ 进程中)就会去加载 JDK 的核心类库,以前我们要配置 CLASSPATH 环境变量应该也是为了去告诉它去哪里加载核心类库所在的 jar 包。现在来说其实是不需要配置 CLASSPATH,JVM 会自动去找到核心类库的 jar 包路径(rt.jar),其实我猜测应该也就是相对于 java 命令本身所在的路径吧(../lib/rt.jar)或者是相对于 $JAVA_HOME 的路径

在我安装的 adoptopenjdk-8.jdk 中,实际上不存在 $JAVA_HOME/lib/rt.jar,只有 $JAVA_HOME/src.zip,BootstrapClassLoader 加载的是这个 zip 包里的核心类库。(jar包其实就是zip格式)

可以使用 -Xbootclasspath 来改变它加载的路径,具体看我前面的文章 classpath和java命令

sun.misc.Launcher

一直传说的类加载器的三层模型:BootstrapClassLoader ---> ExtClassLoader ---> AppClassLoader,其实后面 2 个 classLoader 都是 Launcher 的静态内部类。

首先看下 Launcher 的关键逻辑:

public class Launcher {
        // 关键逻辑,这里去调用构造函数构造一个实例了
        private static Launcher launcher = new Launcher();
      public Launcher() {
        // Create the extension class loader
        ClassLoader extcl;
        try {
            extcl = ExtClassLoader.getExtClassLoader();
        } catch (IOException e) {
            throw new InternalError(
                "Could not create extension class loader", e);
        }

        // Now create the class loader to use to launch the application
        try {
            loader = AppClassLoader.getAppClassLoader(extcl);
        } catch (IOException e) {
            throw new InternalError(
                "Could not create application class loader", e);
        }

        // Also set the context class loader for the primordial thread.
        Thread.currentThread().setContextClassLoader(loader);

        // Finally, install a security manager if requested
        String s = System.getProperty("java.security.manager");
        if (s != null) {
            // init FileSystem machinery before SecurityManager installation
            sun.nio.fs.DefaultFileSystemProvider.create();

            SecurityManager sm = null;
            if ("".equals(s) || "default".equals(s)) {
                sm = new java.lang.SecurityManager();
            } else {
                try {
                    sm = (SecurityManager)loader.loadClass(s).newInstance();
                } catch (IllegalAccessException e) {
                } catch (InstantiationException e) {
                } catch (ClassNotFoundException e) {
                } catch (ClassCastException e) {
                }
            }
            if (sm != null) {
                System.setSecurityManager(sm);
            } else {
                throw new InternalError(
                    "Could not create SecurityManager: " + s);
            }
        }
    }
}
  • private static Launcher launcher = new Launcher(); 初始化的逻辑:

    • 因为 Launcher 属于核心类库,因此它被 BootstrapClassLoader 加载后执行了初始化,因此这里就调用构造方法构造了 Launcher 实例
  • public Launcher() 构造方法的逻辑:

    • 使用 ExtClassLoader.getExtClassLoader() 构造 ExtClassLoader 实例

    • 使用 loader = AppClassLoader.getAppClassLoader(extcl); 构造 AppClassLoader 实例,实际上也就是 AppClassLoader 内部持有 ExtClassLoader 实例

    • Thread.currentThread().setContextClassLoader(loader);

      • 将 AppClassLoader 设置为线程上下文类加载器
    • 使用 java.security.manager 定义的 SecurityManager 类名,使用 AppClassLoader 加载后作为系统的 SecurityManager,如果没有定义,就使用 java.lang.SecurityManager(这一块儿以后再研究)

总结

  • ExtClassLoader 和 AppClassLoader 都是 Launcher 的静态内部类
  • Launcher 被 BootstrapClassLoader 加载初始化后,先后构造了 ExtClassLoader 和 AppClassLoader 实例,然后将 AppClassLoader 设置为线程的上下文类加载器,这就是 Launcher 的主要逻辑
  • AppClassLoader 被 Launcher 持有作为它给出的 ClassLoader
  • AppClassLoader 内部持有 ExtClassLoader 作为上层的 ClassLoader

ExtClassLoader

ExtClassLoader 是用来加载以 javax.* 开头的那些 java 扩展类库,这些类库的 jar 包存在于 $JAVA_HOME/jre/lib/ext 目录下,说白了这个 ExtClassLoader 就是专门去加载这个路径下的 jar 包的,我们把自己的 jar 包扔到这个路径下面也会被它加载,可以使用 java 系统属性:java.ext.dirs 来改变它加载的路径

初始化逻辑

static class ExtClassLoader extends URLClassLoader {
    static {
        ClassLoader.registerAsParallelCapable();
    }
    private static volatile ExtClassLoader instance = null;
            public static ExtClassLoader getExtClassLoader() throws IOException
        {
            if (instance == null) {
                synchronized(ExtClassLoader.class) {
                    if (instance == null) {
                        instance = createExtClassLoader();
                    }
                }
            }
            return instance;
        }

    private static ExtClassLoader createExtClassLoader() throws IOException {
        try {
            // Prior implementations of this doPrivileged() block supplied
            // aa synthesized ACC via a call to the private method
            // ExtClassLoader.getContext().

            return AccessController.doPrivileged(
                new PrivilegedExceptionAction<ExtClassLoader>() {
                    public ExtClassLoader run() throws IOException {
                        final File[] dirs = getExtDirs();
                        int len = dirs.length;
                        for (int i = 0; i < len; i++) {
                            MetaIndex.registerDirectory(dirs[i]);
                        }
                        return new ExtClassLoader(dirs);
                    }
                });
        } catch (java.security.PrivilegedActionException e) {
            throw (IOException) e.getException();
        }
    }
}
  • 被 Launcher 构造之前进行初始化

  • 初始化 ClassLoader.registerAsParallelCapable(); 的逻辑:

    @CallerSensitive
    protected static boolean registerAsParallelCapable() {
        Class<? extends ClassLoader> callerClass =
            Reflection.getCallerClass().asSubclass(ClassLoader.class);
        return ParallelLoaders.register(callerClass);
    }
    
    • 使用反射获取到调用这个方法的类,也就是 ExtClassLoader
    • 将自己注册为可并行的类加载器

初始化主要是注册自己为可并行的类加载器,这种使用反射 Reflection.getCallerClass() 的模式就很有用,注册了之后,ClassLoader 就知道:这个 ClassLoader 可以并行加载,也就是说它的 loadClass 方法可以安全地被多个线程调用去加载类

getExtClassLoader 方法

public static ExtClassLoader getExtClassLoader() throws IOException
{
    if (instance == null) {
        synchronized(ExtClassLoader.class) {
            if (instance == null) {
                instance = createExtClassLoader();
            }
        }
    }
    return instance;
}
  • 双重判空来确保单例
  • 使用 createExtClassLoader 方法构造出单例对象作为自己的静态字段

没啥太多说的,主要是确保只有一个单例,最终逻辑还是在 createExtClassLoader 方法

createExtClassLoader 方法

private static ExtClassLoader createExtClassLoader() throws IOException {
    try {
        // Prior implementations of this doPrivileged() block supplied
        // aa synthesized ACC via a call to the private method
        // ExtClassLoader.getContext().

        return AccessController.doPrivileged(
            new PrivilegedExceptionAction<ExtClassLoader>() {
                public ExtClassLoader run() throws IOException {
                    final File[] dirs = getExtDirs();
                    int len = dirs.length;
                    for (int i = 0; i < len; i++) {
                        MetaIndex.registerDirectory(dirs[i]);
                    }
                    return new ExtClassLoader(dirs);
                }
            });
    } catch (java.security.PrivilegedActionException e) {
        throw (IOException) e.getException();
    }
}
  • 使用 getExtDirs 里获取到的文件路径,最后构造了一个 ExtClassLoader 实例

AppClassLoader

AppClassLoader 是用来加载我们自行传入的应用程序所包含的类的,我们传入的 classpath 最终就会被 AppClassLoader 去加载,通过 java -cp dir1:dir2:dir3:aaa.jar:bbb.jar 传入多个 classpath

初始化逻辑

static class AppClassLoader extends URLClassLoader {
        static {
            ClassLoader.registerAsParallelCapable();
        }
}
  • 被 Launcher 构造之前进行初始化
  • ClassLoader.registerAsParallelCapable(); 将自己注册为可并行的类加载器

getAppClassLoader 方法

public static ClassLoader getAppClassLoader(final ClassLoader extcl)
            throws IOException
        {
            final String s = System.getProperty("java.class.path");
            final File[] path = (s == null) ? new File[0] : getClassPath(s);

            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }

        final URLClassPath ucp;

        /*
         * Creates a new AppClassLoader
         */
        AppClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent, factory);
            ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
            ucp.initLookupCache(this);
        }
  • Launcher 使用 getAppClassLoader 获取一个 AppClassLoader 实例,传入的是 ExtClassLoader
  • 我们通过 java -cp 传递的 classpath,在 java 程序中通过 System.getProperty("java.class.path"); 访问到
  • 将 classpath 转换成 url 的集合,缓存到它继承的 URLClassPath 中去

ClassLoader 类源码分析

说明

源码是 mac 下的 adoptopenjdk-8,主要是进行 ClassLoader 的一些关键逻辑的分析,一些细枝末节和一些跟安全性相关的内容没有仔细去看和了解。主要是着眼于 ClassLoader 加载类的流程,以及它加载的一些机制。

静态变量

  • // The class loader for the system
    // @GuardedBy("ClassLoader.class")
    private static ClassLoader scl;
    
    • 系统类加载器,也就是 AppClassLoader
  • // Set to true once the system class loader has been set
    // @GuardedBy("ClassLoader.class")
    private static boolean sclSet;
    
    • 表示系统类加载器是否已经设置了,一旦 AppClassLoader 被设置给 scl,这个值就是 true

总结

也就是说,ClassLoader 的静态变量也会持有一个系统类加载器,也就是 AppClassLoader

实例变量

  • // The parent class loader for delegation
    // Note: VM hardcoded the offset of this field, thus all new fields
    // must be added *after* it.
    private final ClassLoader parent;
    
    • 每个 ClassLoader 的实现类,都持有一个 parent 的 ClassLoader,因此 ClassLoader 的层次结构类似于一棵树,任何一个 ClassLoader 最后都通过 parent 逐步向上到达树根:null(BootstrapClassLoader)
  • // Maps class name to the corresponding lock object when the current
    // class loader is parallel capable.
    // Note: VM also uses this field to decide if the current class loader
    // is parallel capable and the appropriate lock object for class loading.
    private final ConcurrentHashMap<String, Object> parallelLockMap;
    
    • 这个 parallelLockMap 只有注册了并行加载能力的 ClassLoader 才有,没有注册并行加载能力的 ClassLoader,这个字段的值将会是 null
    • ClassLoader 使用这个字段来判断:定义的具体的 ClassLoader 是否具有并行加载能力(loadClass 方法是否能够被不同线程同时访问)。
    • 如果没有并行加载能力,loadClass 将会是一个同步方法,锁对象就是当前的 ClassLoader 实例。如果有并行加载能力,将会从这个 map 里获取到锁对象(map是类名到锁对象的映射),如果没有锁对象的话(表示当前这个类没有其他线程在加载),就新生成一个 Object 作为锁对象,然后在加载这个特定类的时候使用这个锁对象加锁
    • 也就是说:
      • 默认加载所有的类都使用当前的 ClassLoader 实例加锁
      • 注册了并行加载后,仅在加载 A 类时使用 A 类对应的某个特定锁对象 lockA,加载 B 类时使用 B 类对应的某个锁对象 lockB,这样就大大减小了锁的粒度
  • // The classes loaded by this class loader. The only purpose of this table
    // is to keep the classes from being GC'ed until the loader is GC'ed.
    private final Vector<Class<?>> classes = new Vector<>();
    
    • 每个 ClassLoader 都持有它自己加载过了的所有的类,唯一的目的就是保持一点:这些被加载后生成的 Class 对象不会被 GC 回收掉,因为:这个数组持有所有自己加载了的 Class 对象的强引用
    • findLoadedClass(name),也就是查找当前 ClassLoader 是否已经加载过某个类的方法,并不是依靠这个 classes 来实现的,真正实现这个逻辑的在 native 方法中,也就是 JVM 实现里。因为很显然我们这里只能得到类的名称,并没有办法直接与 Vector<Class<?>> 去查找然后判断自己是否加载过这个类(当然我们也可以自己重写这种逻辑)
  • private final ProtectionDomain defaultDomain =
        new ProtectionDomain(new CodeSource(null, (Certificate[]) null),
                             null, this, null);
    
    • 主要关注点在:构造这个 ProtectionDomain 的时候,把 this 指针,也就是当前 ClassLoader 加入进去了。
    • 当真正让 JVM 去加载类的时候,其实是调用了 defineClass 方法,最终将这个 ProtectionDomain 传递给了这个 native 方法。因此 native 构造出 Class 对象的时候,是可以通过 ProtectionDomain 获取到当前 ClassLoader 对象实例的,所以当我们调用 someClass.getClassLoader() 的时候可以获取到它的类加载器,这个类加载器就是通过 ProtectionDomain 加入进去的
  • // The packages defined in this class loader.  Each package name is mapped
    // to its corresponding Package object.
    // @GuardedBy("itself")
    private final HashMap<String, Package> packages = new HashMap<>();
    
    • 表示这个 ClassLoader 加载过了的所有的包,map的内容是包名到 Package 这个存储了包的定义的映射
    • 我们可以调用 someClassLoader 来获得某个包的定义,如果这个包没有被这个 ClassLoader 加载过,默认的 ClassLoader 实现会将这个包名的查询也转交给持有的上层的 ClassLoader 去查询

总结

  • 每个 ClassLoader 都持有一个 parent 对象,表示它的上层的 ClassLoader,AppClassLoader 上层是 ExtClassLoader,ExtClassLoader 上层是 null(BootstrapClassLoader)
  • 由于这种持有的关系,所以 ClassLoader 是一种树状的关系,最上层的是 BootstrapClassLoader
  • 一个 ClassLoader 的实现类默认是不能并行处理的,在 loadClass 方法中会对当前 ClassLoader 实例加锁。需要在静态初始化代码块中调用:ClassLoader.registerAsParallelCapable(); 将自己注册为可并行的类加载器。此时这个 ClassLoader 只有在加载同一个类的时候会进行线程同步,加载不同的类是可以并行的。
  • 每个 ClassLoader 实例构造出来的每个 Class 实例,都默认绑定了这个 ClassLoader 实例,通过 Class 实例的 getClassLoader 方法可以获取到真正加载它的 ClassLoader 实例(真正 define 这个 Class 的 ClassLoader)
  • 由于委托模式的存在,Class a = classLoaderA.load("xxxName"),最终 a 代表的 Class 不一定是 classLoaderA 加载的,可能是它的 parent 加载的,也就是说 a.getClassLoader() 不一定是 classLoaderA,可能是 classLoaderA.getParent(),或者是 classLoaderA.getParent().getParent()
  • 每个 ClassLoader 实例都保存了它加载过的 package,我们可以传入完整的包名给 ClassLoader 实例,然后获取到这个包名对应的 Package 对象,从而获取包的更多信息

静态方法

registerAsParallelCapable

@CallerSensitive
protected static boolean registerAsParallelCapable() {
    Class<? extends ClassLoader> callerClass =
        Reflection.getCallerClass().asSubclass(ClassLoader.class);
    return ParallelLoaders.register(callerClass);
}
  • 如果一个 ClassLoader 想要实现并行加载,那么在静态初始化的时候,需要调用这个静态方法,将自己注册到并行加载器里面
  • 它的主要逻辑就是使用反射获取调用类:也就是哪个类调用了这个方法(Reflection.getCallerClass(),这个可以值得关注

getSystemResource

public static URL getSystemResource(String name) {
    ClassLoader system = getSystemClassLoader();
    if (system == null) {
        return getBootstrapResource(name);
    }
    return system.getResource(name);
}
  • 获取系统类加载器:AppClassLoader
  • 使用 AppClassLoader 实例的 getResource 方法获取资源 URL
  • 实际上 AppClassLoader 并没有重写 getResource 方法,最后调用的就是 ClassLoader 实例的 getResource 方法
  • 语义是:使用系统类加载器去寻找某个资源,例如 "META-INF/MANIFEST.MF" 就表示找到某个 jar 包下的这个文件,多个 jar 包是可以存在相同的文件的,而这个方法仅仅是找到其中一个,然后就直接返回

getSystemResources

public static Enumeration<URL> getSystemResources(String name)
    throws IOException
{
    ClassLoader system = getSystemClassLoader();
    if (system == null) {
        return getBootstrapResources(name);
    }
    return system.getResources(name);
}
  • 寻找的逻辑与上面一致,唯一的区别是:将所有找到的资源都返回,返回的是一个枚举,我们可以迭代这个枚举从而遍历所有找到的资源

getSystemResourceAsStream

public static InputStream getSystemResourceAsStream(String name) {
    URL url = getSystemResource(name);
    try {
        return url != null ? url.openStream() : null;
    } catch (IOException e) {
        return null;
    }
}
  • 寻找的逻辑与上面一样,唯一的区别是,这里找到的资源返回的是 InputStream,实际上来说仅仅是比上面多了一步,多帮我们调用了一个 url.openStream(),没什么其它好说的

getSystemClassLoader

@CallerSensitive
public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}
  • 获取系统类加载器,也就是 AppClassLoader

总结

  • 具体的 ClassLoader 实现类通过 ClassLoader 的静态方法 registerAsParallelCapable,在将自己注册为可并行加载的类加载器(多线程同时执行这个 ClassLoader 实例的 loadClass 方法)
  • 使用 getSystemResource 和 getSystemResources 方法,我们传入某个资源在某个 jar 包里的完整路径,比如 "META-INF/MANIFEST.MF" 这种,获取到这个资源的 URL
    • 这两个方法基本等价于 AppClassLoader 实例的 getResource/getResources 方法
    • 与类加载的委托机制类似,寻找资源的时候也是先让父加载器去寻找,最后才自己寻找
    • getSystemResource 只找到第一个资源就返回,getSystemResources 则是找到所有资源,然后返回所有 URL 的枚举,我们可以遍历处理
  • getSystemResourceAsStream 方法几乎与 getSystemResource 一致,仅仅是多调用了一个 url.openStream()
  • getSystemClassLoader 获取到系统类加载器 AppClassLoader 实例

实例方法

构造方法

private ClassLoader(Void unused, ClassLoader parent) {
    this.parent = parent;
    if (ParallelLoaders.isRegistered(this.getClass())) {
        parallelLockMap = new ConcurrentHashMap<>();
        package2certs = new ConcurrentHashMap<>();
        assertionLock = new Object();
    } else {
        // no finer-grained lock; lock on the classloader instance
        parallelLockMap = null;
        package2certs = new Hashtable<>();
        assertionLock = this;
    }
}
protected ClassLoader(ClassLoader parent) {
    this(checkCreateClassLoader(), parent);
}
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}
  • 可以看到,前两个构造方法都要传入父类类加载器 parent
  • 第三个构造方法使用 getSystemClassLoader 获取系统类加载器 AppClassLoader 实例

loadClass

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

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 {
                    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;
    }
}
  • 两个方法只是不同的重载,不传入 resolve 参数的 loadClass 方法表示:加载该类,但是并不解析,解析的工作是后续由 JVM 去完成的(也就是我们并不需要显式调用 loadClass(someClass, true))
  • 首先调用 findLoadedClass(name) 判断自己是否加载过这个类
  • 然后委托父加载器 parent 去加载该类
  • 如果加载不到,就自己去加载该类,加载的逻辑就在 findClass 方法中,这个方法的语义是:根据类名找到该类并且加载

findClass

// ClassLoader.java
protected Class<?> findClass(String name) throws ClassNotFoundException {
    throw new ClassNotFoundException(name);
}

// URLClassLoader.java
protected Class<?> findClass(final String name)
    throws ClassNotFoundException
{
    final Class<?> result;
    try {
        result = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Class<?>>() {
                public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
    } catch (java.security.PrivilegedActionException pae) {
        throw (ClassNotFoundException) pae.getException();
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}
  • ClassLoader 的 findClass 方法直接抛出异常,它的语义是让具体的 ClassLoader 一定要重写这个方法,也就是说,将 "根据全类名找到某个类并且加载" 这个动作交给了子类去实现,赋予了子类重写方法的权限。因此也就给我们自定义类加载器留下了一个很重要的口子

  • AppClassLoader 和 ExtClassLoader 都继承自 URLClassLoader,它们俩并未再次重写 findClass 这个方法,而是直接使用的 URLClassLoader 的 findClass 方法

  • String path = name.replace('.', '/').concat(".class");
    Resource res = ucp.getResource(path, false);
    
    • 核心逻辑就是将类名的点号替换为目录的分隔符,然后加上类的扩展名 class ,然后使用 ucp.getResource 去找到这个类文件
      • 实际上 AppClassLoader 的 getResource 的核心逻辑就是 ucp.getResource,ucp 对象里保存了 AppClassLoader 应该加载的 classpath,从这些 classpath 里去寻找资源文件
  • return defineClass(name, res);,真正加载了类并且返回 Class 对象的就是这个 defineClass 方法

    private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            byte[] b = res.getBytes();
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            sun.misc.PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, b, 0, b.length, cs);
        }
    }
    
    • 这里的逻辑主要是根据 Resource 然后去读取这个类的字节码,存放在数组中,最终调用了 ClassLoader 的方法

      protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                           ProtectionDomain protectionDomain)
          throws ClassFormatError
      {
          protectionDomain = preDefineClass(name, protectionDomain);
          String source = defineClassSourceLocation(protectionDomain);
          Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
          postDefineClass(c, protectionDomain);
          return c;
      }
      

getResource

public URL getResource(String name) {
    URL url;
    if (parent != null) {
        url = parent.getResource(name);
    } else {
        url = getBootstrapResource(name);
    }
    if (url == null) {
        url = findResource(name);
    }
    return url;
}
  • 首先调用持有的父类加载器的 getResource 方法
    • 对于 ExtClassLoader 和 AppClassLoader 来说,它们俩都没有重写这个方法
    • 因此它们最终都走到了 else 分支,调用 getBootstrapResource 方法了
    • 也就是说加载资源也遵循父类优先的模式
      • 首先让 parent 去加载资源
      • parent 加载不到的话就自己去加载资源
    • 如果同一个资源有多个的话,最先被找到的那个资源被返回,例如我们可能加载了多个 jar,多个 jar 可能有多个相同的资源,比如 jar 包里都有某个清单文件,此时只有第一个被找到的才返回
    • 寻找的模式是 jar 包里的文件,例如说我们想要找到 java.lang.String 这个类的类文件,可以使用 ClassLoader.getSystemClassLoader().getResource("java/lang/String.class")

getResources

public Enumeration<URL> getResources(String name) throws IOException {
    @SuppressWarnings("unchecked")
    Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
    if (parent != null) {
        tmp[0] = parent.getResources(name);
    } else {
        tmp[0] = getBootstrapResources(name);
    }
    tmp[1] = findResources(name);

    return new CompoundEnumeration<>(tmp);
}

与上面的类似,只不过返回的是这个 ClassLoader 实例的所有找到的资源,上面 getResource 仅仅找到第一个就直接返回

getResourceAsStream

public InputStream getResourceAsStream(String name) {
    URL url = getResource(name);
    try {
        return url != null ? url.openStream() : null;
    } catch (IOException e) {
        return null;
    }
}

与 getResource 基本一致,仅仅是帮我们调用了 url.openStream()

getParent

@CallerSensitive
public final ClassLoader getParent() {
    if (parent == null)
        return null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // Check access to the parent class loader
        // If the caller's class loader is same as this class loader,
        // permission check is performed.
        checkClassLoaderPermission(parent, Reflection.getCallerClass());
    }
    return parent;
}

获取到父加载器,这里还会根据当前调用这个获取父加载器方法的类,检查这个类是否有权限去获取

总结

  • ClassLoader 构造方法决定了,每个类必有一个 parent 父加载器,如果实现的子类 ClassLoader 没有传入 parent 父加载器的话,默认是系统类加载器 AppClassLoader 实例

  • loadClass 方法默认不对类进行解析工作(解析由 JVM 自己完成)

  • loadClass 方法的默认行为是双亲委托,即:一个 ClassLoader 实例 loadClass 的时候,首先交给它的 parent 去 loadClass,如果 parent 加载失败了,自己才去尝试加载这个类,加载的逻辑在 findClass 方法中。这样的双亲委托保证我们使用核心类库的时候使用的是相同的版本(后面详细说明)

  • findClass 是我们定义一个 ClassLoader 实现类的最重要的逻辑,它的语义是

    • 根据一个类名,获取到类的二进制字节数组

    • 最终要负责调用

      protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                           ProtectionDomain protectionDomain)
      

      就实现了加载类的功能

  • getResource 系列的方法,默认也遵循双亲委托的模式,先由 parent 去找到资源,最后再自己去找

类如何与类加载器绑定在一起的

class ClassLoader{
        private final ProtectionDomain defaultDomain =
        new ProtectionDomain(new CodeSource(null, (Certificate[]) null),
                             null, this, null);
    
        protected final Class<?> defineClass(String name, byte[] b, int off, int len,
                                         ProtectionDomain protectionDomain)
        throws ClassFormatError
    {
        protectionDomain = preDefineClass(name, protectionDomain);
        String source = defineClassSourceLocation(protectionDomain);
        Class<?> c = defineClass1(name, b, off, len, protectionDomain, source);
        postDefineClass(c, protectionDomain);
        return c;
    }
}

  • 任何 ClassLoader 的实现类,都有来自于基类 ClassLoader 的 ProtectionDomain 属性
  • 在这个属性中传入了当前实现类对象的指针 new ProtectionDomain(new CodeSource(null, (Certificate[]) null), null, this, null);(倒数第二个 this 代表实现类)
  • define class 的时候,将 defaultDomain 传给了 native 方法,因此通过这个 defaultDomain 与加载这个类的 classloader 绑定在了一起

引用某个类的时候,使用的是什么 ClassLoader

引用某个类的方式

我们有多种方式引用某个类,例如:

  • A a = new A(); 直接引用
  • Class.forName()
  • someClassLoader.loadClass(className)
  • A.class

一个类完整的命名空间其实是:加载这个类的 ClassLoader 实例 + 完整类名

我们在引用一个类的时候其实都隐含了一个 ClassLoader 实例,下面分析这些引用方式用到的 ClassLoader

直接引用

当我们在 A 类中引用 B 类的时候,默认将使用 A 类的类加载器去加载 B 类(但是由于委托模式的存在,最终真正加载 B 类的不一定是 A 类的类加载器)

public class JavaDemoApplication {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        System.out.println("系统启动");
        MyClassLoader myClassLoader = new MyClassLoader(
                "/Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo");
        Class<?> a = myClassLoader.loadClass("Main");
        Method mainMethod = a.getMethod("main",String[].class);
        mainMethod.invoke(null, (Object) null);
    }

    public static class MyClassLoader extends ClassLoader{
        private Path startPath;
        public MyClassLoader(String startPath){
            // MyClassLoader 实例的 parent 加载器将是系统类加载器 AppClassLoader
            super();
            this.startPath = Paths.get(startPath);
        }
        @SneakyThrows
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            Path classPath = startPath.resolve(name.replace('.', '/').concat(".class"));
            byte[] bytes = Files.readAllBytes(classPath);
            return defineClass(name, bytes, 0, bytes.length);
        }
    }
}


// /Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/Main.java
public class Main {
    public static void main(String[] args) {
        A a = new A();
        a.hello();
        System.out.println(a.getClass().getClassLoader().getClass().getName());
    }
}

// /Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/A.java
public class A {
    public void hello(){
        System.out.println("hello, ClassLoader");
    }
}

  • JavaDemoApplication 是我们的主类,并且定义了一个静态内部类 MyClassLoader
  • MyClassLoader 的 parent 类加载器是系统类加载器 AppClassLoader
  • MyClassLoader 加载某个固定路径下的 class 文件
  • 使用 MyClassLoader 加载了 /Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/Main.class
  • 然后调用 Main 类的 main 方法,main 方法中使用 A a = new A(); 引用到了 A 类(/Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/A.class)
  • 此时就会由 Main 类的类加载器 MyClassLoader 去尝试加载 A 类,于是输出中就可以看到 A 类的类加载器是 MyClassLoader

完整输出

系统启动
hello, ClassLoader
com.example.javademo.JavaDemoApplication$MyClassLoader

Class.forName()

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

@CallerSensitive
public static Class<?> forName(String name, boolean initialize,
                               ClassLoader loader)
    throws ClassNotFoundException
{
    Class<?> caller = null;
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        // Reflective call to get caller class is only needed if a security manager
        // is present.  Avoid the overhead of making this call otherwise.
        caller = Reflection.getCallerClass();
        if (sun.misc.VM.isSystemDomainLoader(loader)) {
            ClassLoader ccl = ClassLoader.getClassLoader(caller);
            if (!sun.misc.VM.isSystemDomainLoader(ccl)) {
                sm.checkPermission(
                    SecurityConstants.GET_CLASSLOADER_PERMISSION);
            }
        }
    }
    return forName0(name, initialize, loader, caller);
}

private static native Class<?> forName0(String name, boolean initialize,
                                        ClassLoader loader,
                                        Class<?> caller)
  • Class.forName(String className) 使用的是调用这个方法的类的类加载器来加载这个新类,也就是与直接引用的方式是一致的,并且这个类会被初始化
  • forName(String name, boolean initialize, ClassLoader loader) 使用的是我们手动传入的类加载器来加载这个新类,也就是说我们可以自己决定谁来加载这个类,并且可以控制是否初始化。

总结

  • 在 A 类中引起 B 类的加载的时候,会使用 A 类的类加载器去加载 B 类
  • 主要包括有 A a = new A() 这种直接引用和 Class.forName(className) 这种加载,两者都是使用 A 类的加载器去加载新类

两个不同的类加载器是否会让一个类初始化多次

public class JavaDemoApplication {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        System.out.println("系统启动");
        MyClassLoader myClassLoader = new MyClassLoader(
                "/Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo");
        MyClassLoader2 myClassLoader2 = new MyClassLoader2(
                "/Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo");
        Class<?> a = myClassLoader.loadClass("Main");
        Class<?> b = myClassLoader2.loadClass("Main");
        System.out.println(a.getClassLoader().getClass().getName());
        System.out.println(b.getClassLoader().getClass().getName());
        System.out.println(a == b);
        Method mainMethod = a.getMethod("main",String[].class);
        Method mainMethod2 = b.getMethod("main",String[].class);
        mainMethod.invoke(null, (Object) null);
        mainMethod2.invoke(null, (Object) null);
        System.out.println("系统结束");
    }

    public static class MyClassLoader extends ClassLoader{
        private Path startPath;
        public MyClassLoader(String startPath){
            // MyClassLoader 实例的 parent 加载器将是系统类加载器 AppClassLoader
            super();
            this.startPath = Paths.get(startPath);
        }
        @SneakyThrows
        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            Path classPath = startPath.resolve(name.replace('.', '/').concat(".class"));
            byte[] bytes = Files.readAllBytes(classPath);
            return defineClass(name, bytes, 0, bytes.length);
        }
    }

    public static class MyClassLoader2 extends MyClassLoader{
        public MyClassLoader2(String startPath){
            super(startPath);
        }

        /**
         * 先尝试自己加载,然后才转移给父类
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
        @Override
        protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
            Class<?> aClass = null;
            try {
                aClass = findClass(name);
            } catch (Exception e) {
                return super.loadClass(name, resolve);
            }
            if(resolve){
                resolveClass(aClass);
            }
            return aClass;
        }
    }
}

// /Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/Main.java
public class Main {
    public static void main(String[] args) {
        A a = new A();
        a.hello();
        System.out.println(a.getClass().getClassLoader().getClass().getName());
    }
}

// /Users/czp/workspace/projects/java-demo/src/main/java/com/example/javademo/A.java
public class A {
    public void hello(){
        System.out.println("hello, ClassLoader");
    }
}

输出如下:

系统启动
com.example.javademo.JavaDemoApplication$MyClassLoader
com.example.javademo.JavaDemoApplication$MyClassLoader2
false
Main 类被加载了
hello, ClassLoader
com.example.javademo.JavaDemoApplication$MyClassLoader
Main 类被加载了
hello, ClassLoader
com.example.javademo.JavaDemoApplication$MyClassLoader2
系统结束

总结

  • 两个不同的类加载器可以对一个类初始化多次
  • 归根结底是因为使用了不同的加载器加载一个类之后,它在 JVM 看来压根儿就是毫不相干的两个类,哪怕他们的字节码完全一致
  • 因此当我们使用 2 个不同的 ClassLoader 去加载相同的字节码之后,初始化是可以执行 2 次的
  • 必须要主动使用了 Class 的功能,比如调用它的方法,获取它的字段,这样 JVM 才会主动去初始化这个类

Class.forName() 和 classLoader.loadClass() 的区别

  • Class.forName(className)
    • 约等于 Class.forName(className, true, ClassLoader.getClassLoader(caller))
    • 也就是说,它使用当前调用这个方法的类的类加载器来加载这个 className 对应的类
    • 并且默认会执行类的初始化行为
  • Class.forName(className, initialize, classLoader)
    • 我们可以控制这个类被加载后是否初始化
    • 使用我们传入的 classLoader 来加载这个类
  • classLoader.loadClass()
    • 使用这个类加载器来加载这个类
    • 类不会被初始化,我们可以传入第二个参数表示类是否进行链接操作
  • 所以主要的区别在于 Class.forName(className) 会对类进行初始化操作

默认的线程上下文类加载器

  • 每个线程有一个默认的线程上下文类加载器
  • 每个线程将继承其父线程的上下文类加载器
  • 初始的线程的上下文类加载器是 AppClassLoader
  • 因此一般来说默认的线程上下文类加载器就是 AppClassLoader

为什么要有线程上下文类加载器

  • 当我们在类 A 里引用类 B 的时候,默认使用的是类 A 的类加载器去加载类 B
  • 在 spi 机制中,java 只定义了一组接口,而具体的实现是第三方厂商去定义的,这就出现了一种情况
    • jdk 内部定义使用 spi 功能的时候,肯定是按照接口编程的,那么这些接口都是被 BootstrapClassLoader 加载的
    • 而 spi 的具体实现都是厂商实现的,都是在 classpath 里,应该被 AppClassLoader 加载
    • jdk 内部使用 spi 的时候加载这些第三方类库,BootstrapClassLoader 是无法加载它们的,必须交给别的加载器去加载
    • 因此其实是可以使用 AppClassLoader 去加载的,java 选择添加了线程上下文类加载器的方式,使用上下文类加载器去加载
    • 我的理解是
      • 使用 ClassLoader.getSystemClassLoader 也是可以加载 spi 的
      • 使用线程上下文类加载器是为了添加一种机制:当 BootstrapClassLoader 无法加载某类的时候将控制权交给线程上下文类加载器
      • 实际上此时将控制权交给 ClassLoader.getSystemClassLoader 也就是 AppClassLoader 也是可以的
      • 唯一的不同是线程上下文类加载器会更灵活一点,不同的线程的类加载控制权可以交给不同的类加载器,如果是系统类加载器,那么全都交给了这一个加载器,不够灵活

怎么自定义类加载器

  • 继承 ClassLoader
  • 重写 findClass 方法
  • 如果想要屏蔽双亲委托的行为,可以重写 loadClass 方法
  • findClass 中,最终要调用 defineClass 方法
public class MyClassLoader extends ClassLoader{
    private Path startPath;
    public MyClassLoader(String startPath){
        // MyClassLoader 实例的 parent 加载器将是系统类加载器 AppClassLoader
        super();
        this.startPath = Paths.get(startPath);
    }
    @SneakyThrows
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Path classPath = startPath.resolve(name.replace('.', '/').concat(".class"));
        byte[] bytes = Files.readAllBytes(classPath);
        return defineClass(name, bytes, 0, bytes.length);
    }
}

public class MyClassLoader2 extends MyClassLoader{
    public MyClassLoader2(String startPath){
        super(startPath);
    }

    /**
         * 先尝试自己加载,然后才转移给父类
         * @param name
         * @param resolve
         * @return
         * @throws ClassNotFoundException
         */
    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> aClass = null;
        try {
            aClass = findClass(name);
        } catch (Exception e) {
            return super.loadClass(name, resolve);
        }
        if(resolve){
            resolveClass(aClass);
        }
        return aClass;
    }
}

对双亲委托的一种理解

class A {
   public void some() {
      B b = new B();
      b.call();
  }
}
  • 考虑上面的代码,当我们实例化了一个 A 类的对象,调用它的 some 方法的时候
  • 这个 A 类的实例将会引起 B 类的加载,new B(); 在语义上基本等价于 A.class.getClassLoader().loadClass(“B”).newInstance(),也就是使用 A 类的加载器去尝试加载 B 类
  • 如果 B 类就是 String 类,而 A 类的类加载器不遵守双亲委托,那么 B 类(String)的加载可能将会在中途被截胡,最后加载到的 String 可能就不是 BootstrapClassLoader 加载的 String 了

总结

  • BootstrapClassLoader 加载核心类库

  • AppClassLoader 和 ExtClassLoader 都是被 BootstrapClassLoader 加载的

  • 默认的线程上下文类加载器就是 AppClassLoader

  • 任何一个对象实例,都可以通过 getClass 方法,获取到它的类

  • 任何一个类(Class),都可以通过 getClassLoader,获取到加载这个类的 ClassLoader 实例

  • 类的真正命名空间,是:classloader 实例 + 类的全限定类名。只有这两个都相等的才被认为是同一个类,如果 classloader 实例不同,就会出现 java.lang.String can not cast to java.lang.String 这类的错误

  • 不同的类加载器为类提供了额外的命名空间,所以相同名称的类可以并存在 JAVA 虚拟机中

  • 我们通常在 A 类里加载 B 类,有一个默认的语义

    class A {
       public void some() {
          B b = new B();
          b.call();
      }
    }
    
    • B b = new B(); 在语义上相当于 B b = A.class.getClassLoader().loadClass(“B”).newInstance()
    • 也就是说,我们在一个A类里面加载另一个B类的时候,默认使用的是A类的classloader去加载B类
  • ClassLoader 的并行加载能力就是:一个 ClassLoader 是否可以被不同的线程同时使用 loadClass 去加载类,ExtClassLoader 和 AppClassLoader 都是可以的。我们自定义的类加载器可以使用

    static {
        ClassLoader.registerAsParallelCapable();
    }
    

    这种静态初始化,将 ClassLoader 注册为可以并行加载。如果不注册的话,loadClass 方法将会是一个线程同步的方法,同一时间只能有一个线程执行这个方法

  • Class.forName 会执行类的初始化 ,ClassLoader 的 loadClass 不会初始化,直到第一次真正使用该类才会由 JVM 执行初始化

  • 类可以被初始化多次:前提的这个类被多个不同的类加载器加载

Last updated on 11/8/2020
← java-agent的相关内容java的SecurityManager →
  • 问题
  • 说明
  • 参考
  • BootstrapClassLoader
  • sun.misc.Launcher
    • 总结
  • ExtClassLoader
    • 初始化逻辑
    • getExtClassLoader 方法
    • createExtClassLoader 方法
    • AppClassLoader
    • 初始化逻辑
  • ClassLoader 类源码分析
    • 说明
    • 静态变量
    • 实例变量
    • 静态方法
    • 实例方法
    • 总结
  • 类如何与类加载器绑定在一起的
  • 引用某个类的时候,使用的是什么 ClassLoader
    • 引用某个类的方式
    • 直接引用
    • Class.forName()
    • 总结
  • 两个不同的类加载器是否会让一个类初始化多次
    • 总结
  • Class.forName() 和 classLoader.loadClass() 的区别
  • 默认的线程上下文类加载器
  • 为什么要有线程上下文类加载器
  • 怎么自定义类加载器
  • 对双亲委托的一种理解
  • 总结
影子的知识库
Docs
Getting Started (or other categories)Guides (or other categories)API Reference (or other categories)
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHub
Copyright © 2020 Cen ZhiPeng