JVM(五)虚拟机类加载机制

Java 提供了动态的装载特性;它会在运行时的第一次引用到一个 class 的时候对它进行装载和链接,而不是在编译期进行。JVM 的类装载器负责动态装载,基本上所有的类加载器都是 java.lang.ClassLoader 类的一个实例。

Java 类装载器

有如下几个特点:

  • 层级结构:Java 里的类装载器被组织成了有父子关系的层级结构。Bootstrap 类装载器是所有装载器的父亲。
  • 代理模式:基于层级结构,类的装载可以在装载器之间进行代理。当装载器装载一个类时,首先会检查它是否在父装载器中进行装载了。如果上层的装载器已经装载了这个类,这个类会被直接使用。反之,类装载器会请求装载这个类。
  • 可见性限制:一个子装载器可以查找父装载器中的类,但是一个父装载器不能查找子装载器里的类。
  • 不允许卸载:类装载器可以装载一个类但是不可以卸载它,不过可以删除当前的类装载器,然后创建一个新的类装载器。

每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name)进行搜索来检测这个类是否已经被加载了。如果两个类的全局限定名是一样的,但是如果命名空间不一样的话,那么它们还是不同的类。不同的命名空间表示 class 被不同的类装载器装载。
目前 Java 类装载器的代理模型如下所示:
image.png
当一个类装载器(class loader)被请求装载类时,它首先按照顺序在上层装载器、父装载器以及自身的装载器的缓存里检查这个类是否已经存在。简单来说,就是在缓存里查看这个类是否已经被自己装载过了,如果没有的话,继续查找父类的缓存,直到在 bootstrap 类装载器里也没有找到的话,它就会自己在文件系统里去查找并且加载这个类。

  • 启动类加载器(Bootstrap ClassLoader): 这个类装载器是在 JVM 启动的时候创建的。用于加载 $JAVA_HOME/jre/lib 下面的类库(或者通过参数-Xbootclasspath 指定),由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不能直接通过引用进行操作。
  • 扩展类加载器(ExtClassLoader): 它装载除了基本的 Java API 以外的扩展类。它也负责装载其他的安全扩展功能。在 sun.misc.Launcher 里作为一个内部类 ExtClassLoader 定义的(即 sun.misc.Launcher$ExtClassLoader),ExtClassLoader 会加载 $JAVA_HOME/jre/lib/ext 下的类库(或者通过参数-Djava.ext.dirs 指定)。
  • 系统类加载器(AppClassloader): 如果说 bootstrap class loader 和 extension class loader 负责加载的是 JVM 的组件,那么 system class loader 负责加载的是应用程序类。它负责加载用户在$CLASSPATH里指定的类,是在sun.misc.Launcher里作为一个内部类AppClassLoader定义的(即 sun.misc.Launcher$AppClassLoader),AppClassLoader 会加载 java 环境变量 CLASSPATH 所指定的路径下的类库,而 CLASSPATH 所指定的路径可以通过 System.getProperty(“java.class.path”)获取;当然,该变量也可以覆盖,可以使用参数-cp,例如:java -cp 路径 (可以指定要执行的 class 目录)。
  • 用户自定义类加载器(UserDefined ClassLoader): 这是应用程序开发者用直接用代码实现的类装载器。比如 tomcat 的 StandardClassLoader 属于这一类;当然,大部分情况下使用 AppClassLoader 就足够了。

如果类装载器查找到一个没有装载的类,它会按照下图的流程来装载和链接这个类:
image.png

类装载器阶段

各阶段描述如下:
**Loading: **类的信息从文件中获取并且载入到 JVM 的内存里。
**Verifying:**检查读入的结构是否符合 Java 语言规范以及 JVM 规范的描述。这是类装载中最复杂的过程,并且花费的时间也是最长的。并且 JVM TCK 工具的大部分场景的用例也用来测试在装载错误的类的时候是否会出现错误。
**Preparing:**分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口的信息。
**Resolving:**把这个类的常量池中的所有的符号引用改变成直接引用。
**Initializing:**把类中的变量初始化成合适的值。执行静态初始化程序,把静态变量初始化成指定的值。
JVM 规范定义了上面的几个任务,不过它允许具体执行的时候能够有些灵活的变动。

ClassLoader 加载原理

下面贴下 jdk 关于类加载的源码,上述四种类加载器中 CustomClassLoader 是用户自定义的,BootStrapClassLoader 是 jvm 创建的,就不展示了;这里展示下 AppClassLoader 和 ExtClassLoader 的启动过程,前面介绍过,AppClassLoader 和 ExtClassLoader 都是在 sun.misc.Launcher 里定义的,大家可以下载 openjdk 来查看。

1、Launcher 初始化

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

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

1)通过 ExtClassLoader.getExtClassLoader()创建了 ExtClassLoader;
2)通过 AppClassLoader.getAppClassLoader(ExtClassLoader)创建了 AppClassLoader,并将 ExtClassLoader 设为 AppClassLoader 的 parent ClassLoader;
3)通过 Thread.currentThread().setContextClassLoader(loader)把 AppClassLoader 设为线程的上下文 ClassLoader;
4)根据 jvm 参数-Djava.security.manager 创建安全管理器,”java.security.manager”默认系统属性为空字符串””。

2、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();
        }
    }

    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);
        SharedSecrets.getJavaNetAccess().
            getURLClassPath(this).initLookupCache(this);
    }

    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);
        URL[] urls = super.getURLs();
        File prevDir = null;
        for (int i = 0; i <urls.length; i++) {
            // Get the ext directory from the URL
            File dir = new File(urls[i].getPath()).getParentFile();
            if (dir != null && !dir.equals(prevDir)) {
                // 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(dir, arch), name);
                    if (file.exists()) {
                        return file.getAbsolutePath();
                    }
                }
                // Then check the extension directory
                File file = new File(dir, name);
                if (file.exists()) {
                    return file.getAbsolutePath();
                }
            }
            prevDir = dir;
        }
        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;
    }
}

这里大家关注下 getExtDirs()这个方法,它会获取属性”java.ext.dirs”所对应的值,然后通过系统分隔符分割,然后加载分割后的字符串对应的目录作为 ClassLoader 的类加载库。

3、AppClassLoader 初始化

/**
 * The class loader used for loading from java.class.path.
 * runs in a restricted security context.
 */
static class AppClassLoader extends URLClassLoader {

    static {
        ClassLoader.registerAsParallelCapable();
    }

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

    final URLClassPath ucp;

    /*
     * Creates a new AppClassLoader
     */
    AppClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent, factory);
        ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);
        ucp.initLookupCache(this);
    }

    /**
     * Override loadClass so we can checkPackageAccess.
     */
    public 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));
            }
        }

        if (ucp.knownToNotExist(name)) {
            // The class of the given name is not found in the parent
            // class loader as well as its local URLClassPath.
            // Check if this class has already been defined dynamically;
            // if so, return the loaded class; otherwise, skip the parent
            // delegation and findClass.
            Class<?> c = findLoadedClass(name);
            if (c != null) {
                if (resolve) {
                    resolveClass(c);
                }
                return c;
            }
            throw new ClassNotFoundException(name);
        }

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

首先获取”java.class.path”对应的属性,并转换为 URL[]并设置为 ClassLoader 的类加载库,注意这里的方法入参 classloader 就是 ExtClassLoader,在创 AppClassLoader 会传入 ExtClassLoader 作为 parent ClassLoader。
上面就是 ClassLoader 的启动和初始化过程,后面会把 loader 作为应用程序的默认 ClassLoader 使用,看下面的测试用例:

public static void main(String... args) {
    ClassLoader loader = Test.class.getClassLoader();
    System.err.println(loader);
    while (loader != null) {
        loader = loader.getParent();
        System.err.println(loader);
    }
}

结果输出

sun.misc.Launcher$AppClassLoader@75b84c92
sun.misc.Launcher$ExtClassLoader@1540e19d
null
https://alicharles.oss-cn-hangzhou.aliyuncs.com/static/images/mp_qrcode.jpg
文章目录
  1. Java 类装载器
  2. 类装载器阶段
  3. ClassLoader 加载原理
    1. 1、Launcher 初始化
    2. 2、ExtClassLoader 初始化过程
    3. 3、AppClassLoader 初始化