Java虚拟机类加载器详细介绍

Posted ShrMus

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java虚拟机类加载器详细介绍相关的知识,希望对你有一定的参考价值。

大家好,我是一个爱举铁的程序员Shr

 

本篇文章将详细介绍类加载器,阅读完本篇文章你可能需要20分钟。

 

今天讲述的内容包括:类和类加载器之间的关系,类加载器的分类,类加载器的双亲委派模型。

 

一、什么是类加载器?

类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为类加载器[1]

 

上一篇文章类加载机制中讲的是Java虚拟机如何加载Class文件,而类加载器就是实现“如何加载”的程序代码。

 

二、类和类加载器

类加载器只实现类的加载动作,而类在虚拟机中还会有链接,初始化等阶段,详细阶段请查看前一篇文章Java虚拟机(二)类加载机制

 

对于一个类,都会由这个类本身和加载这个类的类加载器共同确认它在Java虚拟机中的唯一性。

 

举一个例子,一个类被不同的类加载器加载会发生什么。

 

新建一个Hello类,这个类将作为被不同类加载器加载的类。

package com.shrmus.classloader;
public class Hello {
}


新建一个自定义类加载器,继承java.lang.ClassLoader抽象类,重写findClass方法。

package com.shrmus.classloader;
import java.io.IOException;
import java.io.InputStream;
public class MyClassLoader extends ClassLoader{
    @Override
    protected Class<?> findClass(String name){
        byte[] bs = null;
        try {
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream inputStream = getClass().getResourceAsStream(fileName);
            if(inputStream == null) {
                return super.loadClass(name);
            }
            bs = new byte[inputStream.available()];
            inputStream.read(bs);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return defineClass(name, bs, 0, bs.length);
    }
}


新建一个Main类。

package com.shrmus.classloader;
public class Main {
    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader();
        Object newInstance = myClassLoader.findClass("com.shrmus.classloader.Hello").newInstance();
        System.out.println(newInstance.getClass().getClassLoader());
        System.out.println(newInstance instanceof com.shrmus.classloader.Hello);
        Hello hello = new com.shrmus.classloader.Hello();
        System.out.println(hello.getClass().getClassLoader());
        System.out.println(hello instanceof com.shrmus.classloader.Hello);
    }
}


控制台打印结果:

com.shrmus.classloader.MyClassLoader@33909752

false

sun.misc.Launcher$AppClassLoader@6d06d69c

true

 

Main类中,用自定义的类加载器加载Hello类并生成实例。

 

然后第一行打印的是新生成的实例的类加载器。

 

第二行打印语句通过instanceof关键字来看新生成的实例是否还属于原来的类型,控制台打印的是false

 

然后再用new关键字生成一个实例,第三行打印的是这个对象的类加载器,可以看到和第一行打印出来的类加载不一样。

 

第四行打印的是true

 

这就说明了同一个类用不同的类加载器加载的话,即时原来的类型是一样的,最终在虚拟机中却被认为是不一样的。

 

三、类加载器的分类

Java中有三种类加载器,每个类加载器在创建时就加载它们对应的目录。

 

3.1 sun.misc.Launcher

这个类用来启动主应用程序。

 

部分源代码如下:

/**
 * This class is used by the system to launch the main application.
Launcher */

public class Launcher {
    private static URLStreamHandlerFactory factory = new Factory();
    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");
        }
        // 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");
        }
        // 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) {
            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);
            }
        }
    }
    /*
     * Returns the class loader used to launch the main application.
     */

    public ClassLoader getClassLoader({
        return loader;
    }
}


字段bootClassPath的值就是Bootstrap要加载的类库的路径。


System.out.println(System.getProperty("sun.boot.class.path"));


控制台打印结果:

D:\Program Files\Java\jdk1.8.0_152\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\sunrsasign.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_152\jre\classes

 

它加载%Java_HOME%\jre\lib目录下的类库。

 

这个类的构造方法先是创建一个扩展类加载器。

 

然后再将扩展类加载器作为参数创建一个应用程序类加载器。

 

将应用程序类加载器设置成当前线程的上下文加载器。

 

最后,如果有需要,安装一个安全管理器。

 

3.2 扩展类加载器

扩展类加载器sun.misc.Launcher$ExtClassLoader,这个类是sun.misc.Launcher类的内部类。

 

源代码如下:

    /*
     * The class loader used for loading installed extensions.
     */

    static class ExtClassLoader extends URLClassLoader {
        private File[] dirs;
        /**
         * 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();
            }
        }
        void addExtURL(URL url) {
                super.addURL(url);
        }
        /*
         * Creates a new ExtClassLoader for the specified directories.
         */

        public ExtClassLoader(File[] dirs) throws IOException {
            super(getExtURLs(dirs), null, factory);
            this.dirs = dirs;
        }
        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;
        }
        private static URL[] getExtURLs(File[] dirs) throws IOException {
            Vector<URL> urls = new Vector<URL>();
            for (int i = 0; i < dirs.length; i++) {
                String[] files = dirs[i].list();
                if (files != null) {
                    for (int j = 0; j < files.length; j++) {
                        if (!files[j].equals("meta-index")) {
                            File f = new File(dirs[i], files[j]);
                            urls.add(getFileURL(f));
                        }
                    }
                }
            }
            URL[] ua = new URL[urls.size()];
            urls.copyInto(ua);
            return ua;
        }
        /*
         * Searches the installed extension directories for the specified
         * library name. For each extension directory, we first look for
         * the native library in the subdirectory whose name is the value
         * of the system property <code>os.arch</code>. Failing that, we
         * look in the extension directory itself.
         */

        public String findLibrary(String name) {
            name = System.mapLibraryName(name);
            for (int i = 0; i < dirs.length; i++) {
                // Look in architecture-specific subdirectory first
        // Read from the saved system properties to avoid deadlock
        String arch = VM.getSavedProperty("os.arch");
                if (arch != null) {
                    File file = new File(new File(dirs[i], arch), name);
                    if (file.exists()) {
                        return file.getAbsolutePath();
                    }
                }
                // Then check the extension directory
                File file = new File(dirs[i], name);
                if (file.exists()) {
                    return file.getAbsolutePath();
                }
            }
            return null;
        }
        private static AccessControlContext getContext(File[] dirs)
            throws IOException
        
{
            PathPermissions perms =
                new PathPermissions(dirs);
            ProtectionDomain domain = new ProtectionDomain(
                new CodeSource(perms.getCodeBase(),
                    (java.security.cert.Certificate[]) null),
                perms);
            AccessControlContext acc =
                new AccessControlContext(new ProtectionDomain[] { domain });
            return acc;
        }
    }


再看看sun.misc.Launcher类的构造方法中用ExtClassLoader.getExtClassLoader()创建一个扩展类加载器,而sun.misc.Launcher$ExtClassLoader类的getExtClassLoader()中第一条语句调用getExtDirs()方法。

 

getExtDirs()方法中第一条语句获取系统属性java.ext.dirs

 

我们来看看这个系统属性是什么。

 

System.out.println(System.getProperty("java.ext.dirs"));


控制台打印结果:

D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext;C:\Windows\Sun\Java\lib\ext

 

它加载%JAVA_HOME%\jre\lib\ext目录中的类库。或者被java.ext.dirs系统变量指定的路径中的所有类库。

 

3.3 应用程序类加载器

应用程序类加载器sun.misc.Launcher$AppClassLoader,这个类也是sun.misc.Launcher类的内部类。

 

这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以也称为系统类加载器。

 

源代码如下:

    /**
     * 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);
            // Note: on bugid 4256530
            // Prior implementations of this doPrivileged() block supplied
            // a rather restrictive ACC via a call to the private method
            // AppClassLoader.getContext(). This proved overly restrictive
            // when loading  classes. Specifically it prevent
            // accessClassInPackage.sun.* grants from being honored.
            //
            return AccessController.doPrivileged(
                new PrivilegedAction<AppClassLoader>() {
                    public AppClassLoader run() {
                    URL[] urls =
                        (s == null) ? new URL[0] : pathToURLs(path);
                    return new AppClassLoader(urls, extcl);
                }
            });
        }
        /*
         * Creates a new AppClassLoader
         */

        AppClassLoader(URL[] urls, ClassLoader parent) {
            super(urls, parent, factory);
        }
        /**
         * Override loadClass so we can checkPackageAccess.
         */

        public synchronized Class loadClass(String name, boolean resolve)
            throws ClassNotFoundException
        
{
            int i = name.lastIndexOf('.');
            if (i != -1) {
                SecurityManager sm = System.getSecurityManager();
                if (sm != null) {
                    sm.checkPackageAccess(name.substring(0, i));
                }
            }
            return (super.loadClass(name, resolve));
        }
        /**
         * allow any classes loaded from classpath to exit the VM.
         */

        protected PermissionCollection getPermissions(CodeSource codesource)
        
{
            PermissionCollection perms = super.getPermissions(codesource);
            perms.add(new RuntimePermission("exitVM"));
            return perms;
        }
        /**
         * This class loader supports dynamic additions to the class path
         * at runtime.
         *
         * @see java.lang.instrument.Instrumentation#appendToSystemClassPathSearch
         */

        private void appendToClassPathForInstrumentation(String path) {
            assert(Thread.holdsLock(this));
            // addURL is a no-op if path already contains the URL
            super.addURL( getFileURL(new File(path)) );
        }
        /**
         * create a context that can read any directories (recursively)
         * mentioned in the class path. In the case of a jar, it has to
         * be the directory containing the jar, not just the jar, as jar
         * files might refer to other jar files.
         */

        private static AccessControlContext getContext(File[] cp)
            throws java.net.MalformedURLException
        
{
            PathPermissions perms =
                new PathPermissions(cp);
            ProtectionDomain domain =
                new ProtectionDomain(new CodeSource(perms.getCodeBase(),
                    (java.security.cert.Certificate[]) null),
                perms);
            AccessControlContext acc =
                new AccessControlContext(new ProtectionDomain[] { domain });
            return acc;
        }
    }


再看sun.misc.Launcher类的构造方法中用AppClassLoader.getAppClassLoader(extcl)创建一个应用程序类构造器。

 

sun.misc.Launcher$ExtClassLoader类的

getAppClassLoader()方法中获取系统属性java.class.path

 

我们来看看这个系统属性是什么。


System.out.println(System.getProperty("java.class.path"));

 

控制台打印结果:

D:\Program Files\Java\jdk1.8.0_152\jre\lib\resources.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jsse.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jce.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\charsets.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfr.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\access-bridge-64.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\cldrdata.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\dnsns.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jaccess.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jfxrt.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\localedata.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\nashorn.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunec.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunjce_provider.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunmscapi.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunpkcs11.jar;D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\zipfs.jar;F:\workspace-eclipsej2ee\demo_20180611\bin

 

它加载用户类路径(ClassPath)指定的类库。如果应用程序没有自定义类加载器,这个就是程序默认的类加载器。

 

3.4 启动类加载器

启动类加载器在别的书中可能被称为引导类加载器,Bootstrap ClassLoader

 

刚刚说了,sun.misc.Launcher类并不是启动类加载器,只是这个类创建了扩展类加载器和应用程序类加载器。

 

sun.misc.Launcher类由哪个加载器来加载?


ClassLoader classLoader = sun.misc.Launcher.class.getClassLoader();
System.out.println(classLoader);


控制台打印结果:null

 

这个类的类加载器居然是null,为什么?

 

接下来就要讲讲类加载器的双亲委派模型你就知道原因了。

 

四、双亲委派模型

Java虚拟机的角度来讲,只存在两种不同的类加载器,一种是启动类加载器,这个类加载器用C++语言实现,是虚拟机的一部分。一种是所有其他的类加载器,这些类加载器由Java语言实现,独立于虚拟机外部,全都继承抽象类java.lang.ClassLoader[1]

 

但是从Java开发人员的角度来看,类加载器分为刚刚讲的那三种,启动类加载器,扩展类加载器,应用程序类加载器。

 

应用程序都是由这3种类加载器互相配合加载,还可以加入自己定义的类加载器。类加载器之间的关系如下图所示。

这种层次关系称为类加载器的双亲委派模型。这个模型要求除了顶层的启动类加载器之外,其余的类加载器都应该要有自己的父类加载器。

 

这里的类加载器之间的父子关系用组合关系来复用父加载器的代码,不是用继承关系。关于组合关系,我之前写过一篇设计模式(二) - UML类图有讲过组合关系,表示整体和部分的关系,部分不能脱离整体单独存在。

 

所以你再看sun.misc.Launcher类的构造方法,创建应用程序类加载器有一个参数是扩展类加载器

 

你再仔细分析代码就会发现,是将扩展类加载器设置成应用程序类加载器的父加载器,而如果扩展类加载器创建没有成功的话,应用程序类加载器的父加载器就为null

 

举个例子,新建一个Main类,看看它的加载器是哪一个。

package com.shrmus.classloader;
public class Main {
    public static void main(String[] args) throws Exception {
        ClassLoader classLoader = Main.class.getClassLoader();
        System.out.println(classLoader);
    }
}


控制台打印结果:

sun.misc.Launcher$AppClassLoader@6d06d69c

 

Main类被应用程序类加载器加载,是预期的情况,然后打印应用程序类加载器的父加载器看看。

package com.shrmus.classloader;
public class Main {
    public static void main(String[] args) throws Exception {
        ClassLoader appClassLoader = Main.class.getClassLoader();
        System.out.println(appClassLoader);
        ClassLoader parent = appClassLoader.getParent();
        System.out.println(parent);
    }
}


控制台打印结果:

sun.misc.Launcher$AppClassLoader@6d06d69c

sun.misc.Launcher$ExtClassLoader@70dea4e

 

应用程序类加载器的父加载器是扩展类加载器,符合预期,那扩展类加载器的父加载器呢。

package com.shrmus.classloader;
public class Main {
    public static void main(String[] args) throws Exception {
        ClassLoader appClassLoader = Main.class.getClassLoader();
        System.out.println(appClassLoader);
        ClassLoader ExtClassLoader = appClassLoader.getParent();
        System.out.println(ExtClassLoader);
        ClassLoader parent = ExtClassLoader.getParent();
        System.out.println(parent);
    }
}


控制台打印结果:

sun.misc.Launcher$AppClassLoader@6d06d69c

sun.misc.Launcher$ExtClassLoader@70dea4e

null

 

前面两行都符合预期,但是打印扩展类加载器的父加载器却打印null

 

根据双亲委派模型来看,其实扩展类加载器的父加载器应该是启动类加载器,但是Javanull表示启动类加载器。

 

那刚刚在3.4sun.misc.Launcher类的加载器打印出来是null就能明白了吧,这个类是由启动类加载器加载。

 

此时,回到我举这个例子之前,也就是如果扩展类加载器创建没有成功的话,应用程序类加载器的父加载器就设置null了,就是启动类加载器,这样就不符合双亲委派模型了。

 

五、双亲委派模型的工作过程

如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才尝试自己去加载[1]

 

六、双亲委派模型的实现

实现代码在java.lang.ClassLoaderloadClass()方法中,代码如下。

    protected synchronized Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        // 首先,检查请求的类是否已经被加载过了
        Class c = findLoadedClass(name);
        if (c == null) {
            try {
                if (parent != null) {
// 父加载器不为空,调用父加载器的findClass方法
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClass0(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果父加载器抛出异常说明父类加载器无法加载
                // 调用本身的findClass方法来加载
                c = findClass(name);
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }


先检查类是否已经被加载过,如果没有就调用父加载器的loadClass方法,如果父加载器为空就调用启动类加载器作为父加载器。

 

如果父加载器加载失败,再调用自己的findClass方法加载。

 

七、自定义类加载器

自定义类加载器需要继承java.lang.ClassLoader抽象类,建议重写findClass()方法。在前面讲类和类加载器时写了一个自定义类加载器,这里就不重复了。

 

总结

类加载器有三种,启动类加载器,扩展类加载器,应用程序类加载器。

 

类加载器的双亲委派模型,一个类被加载的时候会一层一层传送到顶层的启动类加载器,如果启动类加载器不能加载这个类,再一层一层传送给子类加载器加载。

 

关于类加载器的介绍就到这里了,后面有了更深的了解再来补充。

 

参考文献

[1] 周志明.深入理解Java虚拟机:JVM高级特性与最佳实践[M].机械工业出版社,2013

 

以上是关于Java虚拟机类加载器详细介绍的主要内容,如果未能解决你的问题,请参考以下文章

Java类加载器学习笔记

java jvm虚拟机类加载器

Java虚拟机类加载机制——案例分析

深度分析:Java虚拟机类加载机制过程与类加载器

深度分析:Java虚拟机类加载机制过程与类加载器

深度分析:Java虚拟机类加载机制过程与类加载器