写点什么

🔎【Java 源码探索】深入浅出的分析 ClassLoader

发布于: 2021 年 05 月 29 日
🔎【Java 源码探索】深入浅出的分析ClassLoader

每日一句

在人生的道路上,即使一切都失去了,只要一息尚存,你就没有丝毫理由绝望。因为失去的一切,又可能在新的层次上复得。

前提概要

Java 体系中的所有类,必须以【class 字节码文件】必须被装载到 JVM 中才能运行,这个装载工作是由 JVM 中的类装载器完成的,类装载器所做的工作实质是把 class 字节码文件从存储介质(网络、硬盘、数据库等多元化方式)读取到 JVM 内存中,JVM 在加载类的时候,都是通过 ClassLoader 的 loadClass()方法来加载 class 字节码的,Java 的类加载器使用双亲委派模式进行加载类。

官方给出 ClassLoader 功能翻译为

ClassLoader 类是一个抽象类

/** * A class loader is an object that is responsible for loading classes. The * class <tt>ClassLoader</tt> is an abstract class.  Given the <a * href="#name">binary name</a> of a class, a class loader should attempt to * locate or generate data that constitutes a definition for the class.  A * typical strategy is to transform the name into a file name and then read a * "class file" of that name from a file system.**/public abstract class ClassLoader
复制代码

大致意思如下(个人补充了完整信息)

  • Classloader 是一个负责加载 classes 的对象,ClassLoader 类是一个抽象类,给定类的二进制名称(全限定名),尝试定位或者产生一个 class 的元数据信息(class 静态常量池中的元数据)存放到方法区中的运行时常量池以及字符串常量池中。并且创建一个 class 实例对象指向该方法区内存的地址。

  • 我们常用的解析方式策略就是将名称转换为物理位置及文件名,然后定位到文件系统等相关媒介中,并且读取该名称的“class 类文件”

前提概要

为了更好的理解类的加载机制,我们来深入研究一下 ClassLoader 和他的 loadClass()方法

ClassLoader 分类

Java 系统自带有三个类加载器

Bootstrap ClassLoader




最顶层的加载类,主要加载核心类库,%JRE_HOME%\lib 下的 rt.jar、resources.jar、charsets.jar 等


  • Java 命令行提供了如何扩展 bootStrap 级别加载 class 的简单方法

  • 另外需要注意的是可以通过启动 JVM 时指定 -Xbootclasspath路径来改变 Bootstrap ClassLoader 的加载目录

  • 语法如下:

  • -Xbootclasspath: 完全取代基本核心的 Java class 搜索路径,不常用,否则要重新写所有 Java 核心 class。

  • -Xbootclasspath/a: 被指定的文件追加到默认的 bootstrap 路径中。

  • -Xbootclasspath/p: 前缀在核心 class 搜索路径前面。不常用,避免引起不必要的冲突。

  • -Dsun.boot.class.path=XXXX


分隔符与 classpath 参数类似,unix 使用:号,windows 使用;号,这里以 unix 为例


 java -Xbootclasspath/a:/usrhome/thirdlib.jar: -jar yourJarExe.jar
复制代码




Extention ClassLoader(扩展的类加载器)


加载目录 %JRE_HOME%\lib\ext 目录下的 jar 包和 class 文件。还可以加载-D java.ext.dirs 选项指定的目录。


可以采用 java.ext.dirs 这个虚拟机参数选项进行设置。




Appclass Loader(系统型类加载器)


也称为 SystemAppClass 加载当前应用的 classpath 的所有类

ClassLoader 从继承关系

为了更好的理解,我们可以查看源码。看 sun.misc.Launcher,它是一个 java 虚拟机的入口应用。


public class Launcher {    private static Launcher launcher = new Launcher();    private static String bootClassPath = System.getProperty("sun.boot.class.path");    public static Launcher getLauncher() {        return launcher;    }    private ClassLoader loader;    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);        }        //设置AppClassLoader为线程上下文类加载器,这个文章后面部分讲解        Thread.currentThread().setContextClassLoader(loader);    }
/* * Returns the class loader used to launch the main application. */ public ClassLoader getClassLoader() { return loader; }
/* * The class loader used for loading installed extensions. */
static class ExtClassLoader extends URLClassLoader {}
/** * The class loader used for loading from java.class.path. * runs in a restricted security context. */ static class AppClassLoader extends URLClassLoader {}
复制代码


源码有精简,我们可以得到相关的信息。


  1. Launcher 初始化了 ExtClassLoader 和 AppClassLoader

  2. Launcher 中并没有看见 BootstrapClassLoader但通过 System.getProperty("sun.boot.class.path")得到了字符串 bootClassPath,这个应该就是 BootstrapClassLoader 加载的 jar 包路径。


我们可以先代码测试一下 sun.boot.class.path 是什么内容。


System.out.println(System.getProperty("sun.boot.class.path"));
复制代码


得到的结果是:


C:\Program Files\Java\jre1.8.0_91\lib\resources.jar;C:\Program Files\Java\jre1.8.0_91\lib\rt.jar;C:\Program Files\Java\jre1.8.0_91\lib\sunrsasign.jar;C:\Program Files\Java\jre1.8.0_91\lib\jsse.jar;C:\Program Files\Java\jre1.8.0_91\lib\jce.jar;C:\Program Files\Java\jre1.8.0_91\lib\charsets.jar;C:\Program Files\Java\jre1.8.0_91\lib\jfr.jar;C:\Program Files\Java\jre1.8.0_91\classes
复制代码


可以看到,这些全是 JRE 目录下的 jar 包或者是 class 文件。

ExtClassLoader 源码
  /*   * The class loader used for loading installed extensions.   */    static class ExtClassLoader extends URLClassLoader {
static { ClassLoader.registerAsParallelCapable(); }
/** * create an ExtClassLoader. The ExtClassLoader is created * within a context that limits which files it can read */ public static ExtClassLoader getExtClassLoader() throws IOException { final File[] dirs = getExtDirs(); 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 { 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(); } }
private static File[] getExtDirs() { String s = System.getProperty("java.ext.dirs"); File[] dirs; if (s != null) { StringTokenizer st = new StringTokenizer(s, File.pathSeparator); int count = st.countTokens(); dirs = new File[count]; for (int i = 0; i < count; i++) { dirs[i] = new File(st.nextToken()); } } else { dirs = new File[0]; } return dirs; } ...... }
复制代码


指定-D java.ext.dirs 参数来添加和改变 ExtClassLoader 的加载路径。这里我们通过可以编写测试代码


System.out.println(System.getProperty("java.ext.dirs"));
复制代码


结果如下:


C:\Program Files\Java\jre1.8.0_91\lib\ext;C:\Windows\Sun\Java\lib\ext
复制代码
AppClassLoader 源码
   /**     * The class loader used for loading from java.class.path.     * runs in a restricted security context.     */    static class AppClassLoader extends URLClassLoader {        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);                }            });        }        ......    }
复制代码


可以看到 AppClassLoader 加载的就是 -Djava.class.path 下的路径。我们同样打印它的值。


System.out.println(System.getProperty("java.class.path"));
复制代码


结果如下:


D:\workspace\ClassLoaderDemo\bin
复制代码


这个路径其实就是当前 java 工程目录 bin,里面存放的是编译生成的 class 文件。


自此我们已经知道了 BootstrapClassLoader、ExtClassLoader、AppClassLoader 实际是查阅相应的环境属性(-Xbootclasspath[/a/p]:)sun.boot.class.path、java.ext.dirs 和 java.class.path 来加载资源文件的



ClassLoader 加载顺序

  • 我们看到了系统的 3 种类加载器,但我们可能不知道具体哪个先行呢?这与我们的加载方式模型来决定的:上面说过 Java 使用的是双亲委托模型

  • Java 的类加载器查找 class 和 resource 时,是通过“委托模式”进行的,它首先判断这个 class 是不是已经加载成功,如果没有的话它并不是自己进行查找,而是先通过父加载器,然后递归下去,直到 Bootstrap ClassLoader。

  • 如果 Bootstrap ClassLoader 也没有加载过此 class 实例,那么它就会从它指定的路径中去查找,如果查找成功则返回,如果没有查找成功则交给子类加载器,也就是 ExtClassLoader,这样类似操作直到终点,也就是我上图中的红色箭头示例。这种机制就叫做双亲委托。

什么是双亲委派机制?

整个流程可以如下图所示:


  • 如果当前的类加载器没有查询到这个 class 对象已经加载就请求父加载器(不一定是父类)进行操作,然后以此类推。


具体流程描述



  1. 一个 AppClassLoader 查找资源时,先看看缓存是否有,缓存有从缓存中获取,否则委托给父加载器。

  2. 递归,重复第 1 部的操作。

  3. 如果 ExtClassLoader 也没有加载过,则由 Bootstrap ClassLoader 出面,它首先查找缓存,如果没有找到的话,就去找自己的规定的路径下,也就是 sun.mic.boot.class 下面的路径。找到就返回,没有找到,让子加载器自己去找。

  4. Bootstrap ClassLoader 如果没有查找成功,则 ExtClassLoader 自己在 java.ext.dirs 路径中去查找,查找成功就返回,查找不成功,再向下让子加载器找。

  5. ExtClassLoader 查找不成功,AppClassLoader 就自己查找,在 java.class.path 路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。


上面的序列,详细说明了双亲委托的加载流程。我们可以发现委托是从下向上,然后具体查找过程却是自上至下。

加载类过程

首先加载类是通过 Classloader 中的 loadClass 方法加载,Classloader 部分代码


loadClass


JDK 文档中是这样写的,通过指定的全限定类名加载 class,它通过同名的 loadClass(String,boolean)方法。


protected Class<?> loadClass(String name, boolean resolve)        throws ClassNotFoundException {        synchronized (getClassLoadingLock(name)) {            // 首先,检测是否已经加载            Class<?> c = findLoadedClass(name);            if (c == null) {                long t0 = System.nanoTime();                try {                    if (parent != null) {                        //父加载器不为空则调用父加载器的loadClass                        c = parent.loadClass(name, false);                    } else {                        //父加载器为空则调用Bootstrap Classloader                        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();                    //父加载器没有找到,则调用findclass                    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()                resolveClass(c);            }            return c;        }    }
// 篡改双亲委托 @Override protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { Class<?> clazz = findLoadedClass(className); if (clazz == null) { clazz = findClass(className); } return clazz; }
// 主要用于覆盖此方法去控制双亲委托下的加载实现 protected final Class<?> findLoadedClass(String name) { ClassLoader loader; if (this == BootClassLoader.getInstance()) loader = null; else loader = this; return VMClassLoader.findLoadedClass(loader, name); }
复制代码


上面是方法原型,一般实现这个方法的步骤是:


  1. 执行 findLoadedClass(String)去检测这个 class 是不是已经加载过了,直接获取当前的类加载器(系统类加载器)

  2. 执行父加载器的 loadClass 方法。如果父加载器为 null,则 jvm 内置的加载器去替代,也就是 Bootstrap ClassLoader。这也解释了 ExtClassLoader 的 parent 为 null,但仍然说 Bootstrap ClassLoader 是它的父加载器

  3. 如果向上委托父加载器没有加载成功,则通过 findClass(String)查找。

  4. 如果 class 在上面的步骤中找到了,参数 resolve 又是 true 的话,那么 loadClass()又会调用 resolveClass(Class)这个方法来生成最终的 Class 对象。 我们可以从源代码看出这个步骤。


另外,要注意的是如果要编写一个 classLoader 的子类,也就是自定义一个 classloader,建议覆盖 findClass()方法,而不要直接改写 loadClass()方法。


if (parent != null) {    //父加载器不为空则调用父加载器的loadClass    c = parent.loadClass(name, false);} else {    //父加载器为空则调用Bootstrap Classloader    c = findBootstrapClassOrNull(name);}
复制代码


  • 前面说过 ExtClassLoader 的 parent 为 null,所以它向上委托时,系统会为它指定 Bootstrap ClassLoader。


synchronized (getClassLoadingLock(name)) 看到这行代码,我们能知道的是,这是一个同步代码块,那么 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;    }
复制代码


  • 以上是 getClassLoadingLock(name)方法的实现细节,我们看到这里用到变量 parallelLockMap ,根据这个变量的值进行不同的操作,如果这个变量是 Null,那么直接返回 this,如果这个属性不为 Null,那么就新建一个对象,然后在调用一个 putIfAbsent(className, newLock);

  • 那么这个 parallelLockMap 变量又是哪来的那,我们发现这个变量是 ClassLoader 类的成员变量:


private final ConcurrentHashMap<String, Object> parallelLockMap;
复制代码


这个变量的初始化工作在 ClassLoader 的构造函数中:


private ClassLoader(Void unused, ClassLoader parent) {        this.parent = parent;        if (ParallelLoaders.isRegistered(this.getClass())) {            parallelLockMap = new ConcurrentHashMap<>();            package2certs = new ConcurrentHashMap<>();            domains =                Collections.synchronizedSet(new HashSet<ProtectionDomain>());            assertionLock = new Object();        } else {            // no finer-grained lock; lock on the classloader instance            parallelLockMap = null;            package2certs = new Hashtable<>();            domains = new HashSet<>();            assertionLock = this;        }    }
复制代码


这里我们可以看到构造函数根据一个属性 ParallelLoaders 的 Registered 状态的不同来给 parallelLockMap 赋值。


在 ClassLoader 类中包含一个静态内部类 private static class ParallelLoaders,在 ClassLoader 被加载的时候这个静态内部类就被初始化。这个静态内部类的代码我就不贴了,直接告诉大家什么意思,sun 公司是这么说的:Encapsulates the set of parallel capable loader types,意识就是说:封装了并行的可装载的类型的集合。


上面这个说的是不是有点乱,那让我们来整理一下:


  • 首先,在 ClassLoader 类中有一个静态内部类 ParallelLoaders,他会指定的类的并行能力,如果当前的加载器被定位为具有并行能力,那么他就给 parallelLockMap 定义,就是 new 一个 ConcurrentHashMap<>(),那么这个时候,我们知道如果当前的加载器是具有并行能力的,那么 parallelLockMap 就不是 Null,这个时候,我们判断 parallelLockMap 是不是 Null,如果他是 null,说明该加载器没有注册并行能力,那么我们没有必要给他一个加锁的对象,getClassLoadingLock 方法直接返回 this,就是当前的加载器的一个实例。

  • 如果这个 parallelLockMap 不是 null,那就说明该加载器是有并行能力的,那么就可能有并行情况,那就需要返回一个锁对象。然后就是创建一个新的 Object 对象,调用 parallelLockMap 的 putIfAbsent(className, newLock)方法,这个方法的作用是:

  • 根据传进来的 className,检查该名字是否已经关联了一个 value 值,如果已经关联过 value 值,那么直接把他关联的值返回,如果没有关联过值的话,那就把我们传进来的 Object 对象作为 value 值,className 作为 Key 值组成一个 map 返回。

  • 无论 putIfAbsent 方法的返回值是什么,都把它赋值给我们刚刚生成的那个 Object 对象。 这个时候,我们来简单说明一下 getClassLoadingLock(String className)的作用,就是: 为类的加载操作返回一个锁对象。

  • 为了向后兼容,这个方法这样实现:如果当前的 classloader 对象注册了并行能力,方法返回一个与指定的名字 className 相关联的特定对象,否则,直接返回当前的 ClassLoader 对象。

双亲委派机制的作用
  1. 避免重复加载,当父类加载器已经加载了该类的时候,就没有必要子类加载器再加载一次了。

  2. 首先,类加载器是按照级别层层往下加载的,当下层的加载器去加载某一个类时,有可能上层的加载已经加载过的,比如 FrameWork 层的加载被 BootClassLoader 加载过,下层不用再去加载了

  3. 安全性考虑,防止核心 API 库被随意篡改。

  4. 系统类加载器已经加载过了 FrameWork 层了类,如果我们自己再写一个系统级别的类,创建包 java.lang,创建一个自己的 String 类,类加载器去加载这个类覆盖了原本的 java.lang 下的 String,那么这个时候使用 String 整个应用就出问题了


package java.lang;
class String {
@NonNull @Override public String toString() { return ""; }
@Override public boolean equals(@Nullable Object obj) { return false; }}
复制代码


而因为双亲委派机制,加载这个 String 类之前,调用 parent 的 BootClassloader,判断已经加载过 String 类,这个类其实不会再去找了,解决了被篡改的问题。

类加载的动态性体现

一个应用程序总是由 n 多个类组成,Java 程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到 jvm 中,其它类等到 jvm 用到的时候再加载,这样的好处是节省了内存的开销,因为 java 最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是 java 动态性的一种体现。

类的加载过程
  • 装载:通过“类全路径名”查找并加载类的二进制数据,并且生成 class 实例对象指向对应的方法区的内存数据结构。

  • 链接:把类的二进制数据合并到 JVM 中(主要作为校验阶段中的 class 文件格式校验阶段完成存入方法区内存中)

  • 验证:确保被加载类的正确性,确保加载内容不危害虚拟机;

  • 准备:为类的静态变量分配内存,并将其初始化为默认值,常量直接赋值;

  • 解析:把类中的符号引用转换为直接引用;

  • 初始化:直接进行先关的静态类构造器,实现相关的相关的类属性和类代码块的执行和赋值操作。

发布于: 2021 年 05 月 29 日阅读数: 580
用户头像

我们始于迷惘,终于更高水平的迷惘。 2020.03.25 加入

🏆 【酷爱计算机技术、醉心开发编程、喜爱健身运动、热衷悬疑推理的”极客狂人“】 🏅 【Java技术领域,MySQL技术领域,APM全链路追踪技术及微服务、分布式方向的技术体系等】 🤝未来我们希望可以共同进步🤝

评论

发布
暂无评论
🔎【Java 源码探索】深入浅出的分析ClassLoader