Java Review - ClassLoader

ClassLoader就是类加载器。ClassLoader的作用就是将class文件加载到jvm虚拟机中去。jvm启动时,并不会一次性加载所有的class文件,而是按需动态加载。


Class文件

平时我们在IDE上编写的都是.java文件,.java文件并不能直接在JVM上运行,例子:

public class Main {
    //运行入口main函数
    public static void main(String[] args) {
        System.out.println("Hello world!");
    }
}

java文件


编译文件


生成.class文件


运行.class文件


.class文件是字节码格式文件,java虚拟机不能识别.java源文件,只能识别运行.class文件,因此我们需要用javac将.java转换为.class文件。


JAVA类加载流程

三大类加载器:

  • Bootstrap ClassLoader 最顶层的加载类,主要加载 核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
  • Extention ClassLoader 扩展的类加载器,加载 目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
  • Appclass Loader也称为SystemAppClass 加载 当前应用的classpath的所有类。

PS:Bootstrap ClassLoader可以通过java -Xbootclasspath/a:path来修改加载的目录,而Extention ClassLoader可以通过-D java.ext.dirs来修改加载的目录


入口源码

查看精简源码,以下是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包路径。

为了验证刚才说的第2点,我们运行下程序:


果然都是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;
    }

......
}

前面说过,ExtClassLoader可以通过java -Xbootclasspath/a:path来修改加载的目录,我们打印下String s = System.getProperty("java.ext.dirs");

果然都是Extensions下的jar包或class文件


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);
            }
        });
    }

    ......
}

按照前面的规矩,打印final String s = System.getProperty("java.class.path");

图上指的路径,就是项目存放编译生成的class的路径

此时:我们已经知道BootstrapClassLoader、ExtClassLoader、AppClassLoader实际就是查询相应环境属性sun.boot.class.pathjava.ext.dirsjava.class.path来加载资源文件的。

我们在跑个例子:

Main.class这个类的类加载为AppClassLoader,那!为啥String.class却报错了,然道是Stirng.class这个类没有类加载器加载。答案是否定的,String.class不仅有类加载器加载,且是Bootstrap ClassLoader加载的。


每个类加载器都有一个父加载器

比如Main.class的父加载器是AppClassLoader,那AppClassLoader的父加载器呢?可以使用getParent

很明显看出AppClassLoader的父加载器就是ExtClassLoader,那ExtClassLoader的父加载器呢?

又是空指针,然道ExtClassLoader没有父加载器?


父加载器不是父类

static class ExtClassLoader extends URLClassLoader {}
static class AppClassLoader extends URLClassLoader {}

ExtClassLoader和AppClassLoader其实都是URLClassLoader的子类,为啥AppClassLoader的getParent()得到的却是ExtClassLoader实例呢?我们先来看看URLClassLoader。

URLClassLoader的源码中并没有找到getParent()方法。这个方法在ClassLoader.java中。

ClassLoader源码

public abstract class ClassLoader {

    // 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;
    // The class loader for the system
        // @GuardedBy("ClassLoader.class")
    private static ClassLoader scl;

    private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        ...
    }
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    public final ClassLoader getParent() {
        if (parent == null)
            return null;
        return parent;
    }
    public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        return scl;
    }

    private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                Throwable oops = null;
                //通过Launcher获取ClassLoader
                scl = l.getClassLoader();
                try {
                    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
                } catch (PrivilegedActionException pae) {
                    oops = pae.getCause();
                    if (oops instanceof InvocationTargetException) {
                        oops = oops.getCause();
                    }
                }
                if (oops != null) {
                    if (oops instanceof Error) {
                        throw (Error) oops;
                    } else {
                        // wrap the exception
                        throw new Error(oops);
                    }
                }
            }
            sclSet = true;
        }
    }
}

其实getParent()实际上就是ClassLoader的属性parent,parent的赋值是在ClassLoader对象的构造方法中,有两种情况:

  1. 外部类指定了ClassLoader时,则ClassLoader的parent就是指定的ClassLoader
  2. 外部类未指定时,由getSystemClassLoader()方法生成,结合前面sun.misc.Laucher源码的getClassLoader(),返回的是AppCLassLoader。

总结: 一个ClassLoader创建时如果没有指定parent,那么它的parent默认就是AppClassLoader。


我们在回到前面Lanucher里的关键代码:

ClassLoader extcl;
extcl = ExtClassLoader.getExtClassLoader();
loader = AppClassLoader.getAppClassLoader(extcl);

前面我已经证明了AppClassLoader的parent是ExtClassLoader实例,而ExtClassLoader并没有对parent赋值。它调用了父类URLClassLoader的构造方法并传递3个参数。

//单参数构造函数
public ExtClassLoader(File[] dirs) throws IOException {
     super(getExtURLs(dirs), null, factory);   
}
//三参数构造函数
public  URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory) {
     super(parent);
}

这里也就可以证实,ExtClassLoader的parent为null。也能解释前面为啥ExtClassLoader调用getParent()时会抛错了。

ExtClassLoader既然parent为空,为什么我们还是说Boostrap ClassLoader是它的父加载器呢?

Bootstrap ClassLoader是由C++编写的。

Bootstrap ClassLoader是由C/C++编写的,它本身是虚拟机的一部分,所以它并不是一个JAVA类,也就是无法在java代码中获取它的引用,JVM启动时通过Bootstrap类加载器加载rt.jar等核心jar包中的class文件,之前的int.class,String.class都是由它加载。

JVM初始化sun.misc.Launcher并创建Extension ClassLoader和AppClassLoader实例。并将ExtClassLoader设置为AppClassLoader的父加载器。Bootstrap没有父加载器,但是它却可以作为一个ClassLoader的父加载器。比如ExtClassLoader。这也可以解释之前通过ExtClassLoader的getParent方法获取为Null的现象。

类的加载 双亲委托

一个类加载器找class和recource时,是通过“委托模式”进行。它首先判断这个class是不是已经加载成功,如果有直接返回,如果没有就通过父加载器查找,不断递归,直到Bootstrap ClassLoader,如果Bootstrap找到了,直接返回,如果没有一级级返回,最后到底自身去查找这些对象。这种机制就叫双亲委托。

流程:


蓝色的代表类加载器向上委托的方向,如果当前的类加载器没有查询到这个class对象已经加载就请求父加载器(不一定是父类)进行操作,然后以此类推。直到Bootstrap ClassLoader。

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

  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路径下查找。找到就返回。如果没有找到就让子类找,如果没有子类会怎么样?抛出各种异常。

上面的序列,详细说明了双亲委托的加载流程。委托是从下向上,查找过程却是自上至下。

从上面两张图可以直观的看出类加载的大致过程。若要了解更细点我们还得知道几个重要方法loadClass()findLoadedClass()findClass()defineClass()

重要方法 loadClass( )

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();
                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;
    }
}
  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对象

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

PS:本文整理自以下博客
一看你就懂,超详细java中的ClassLoader详解
若有发现问题请致邮 caoyanglee92@gmail.com

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容