美文网首页
Java Review - ClassLoader

Java Review - ClassLoader

作者: 你需要一台永动机 | 来源:发表于2019-08-21 15:44 被阅读0次

    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

    相关文章

      网友评论

          本文标题:Java Review - ClassLoader

          本文链接:https://www.haomeiwen.com/subject/eyfisctx.html