7

Java源码分析专题系列之【ClassLoader】深入浅出的源码分析

 3 years ago
source link: https://my.oschina.net/liboware/blog/5062801
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

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

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. 避免重复加载,当父类加载器已经加载了该类的时候,就没有必要子类加载器再加载一次了。

    • 首先,类加载器是按照级别层层往下加载的,当下层的加载器去加载某一个类时,有可能上层的加载已经加载过的,比如FrameWork层的加载被BootClassLoader加载过,下层不用再去加载了
  2. 安全性考虑,防止核心API库被随意篡改。

    • 系统类加载器已经加载过了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文件格式校验阶段完成存入方法区内存中)

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

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK