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

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

层级结构Java里的类装载器被组织成了有父子关系的层级结构。Bootstrap类装载器是所有装载器的父亲。

代理模式:基于层级结构,类的装载可以在装载器之间进行代理。当装载器装载一个类时,首先会检查它是否在父装载器中进行装载了。如果上层的装载器已经装载了这个类,这个类会被直接使用。反之,类装载器会请求装载这个类。

可见性限制:一个子装载器可以查找父装载器中的类,但是一个父装载器不能查找子装载器里的类。

不允许卸载:类装载器可以装载一个类但是不可以卸载它,不过可以删除当前的类装载器,然后创建一个新的类装载器。

每个类装载器都有一个自己的命名空间用来保存已装载的类。当一个类装载器装载一个类时,它会通过保存在命名空间里的类全局限定名(Fully Qualified Class Name)进行搜索来检测这个类是否已经被加载了。如果两个类的全局限定名是一样的,但是如果命名空间不一样的话,那么它们还是不同的类。不同的命名空间表示class被不同的类装载器装载。

目前Java类装载器的代理模型如下所示:

当一个类装载器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 loaderextension 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: 这是应用程序开发者用直接用代码实现的类装载器。比如tomcatStandardClassLoader属于这一类;当然,大部分情况下使用AppClassLoader就足够了。

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

 

每个阶段的描述如下:

Loading:  类的信息从文件中获取并且载入到JVM的内存里。

Verifying检查读入的结构是否符合Java语言规范以及JVM规范的描述。这是类装载中最复杂的过程,并且花费的时间也是最长的。并且JVM TCK工具的大部分场景的用例也用来测试在装载错误的类的时候是否会出现错误。

Preparing分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口的信息。

Resolving把这个类的常量池中的所有的符号引用改变成直接引用。

Initializing把类中的变量初始化成合适的值。执行静态初始化程序,把静态变量初始化成指定的值。

JVM规范定义了上面的几个任务,不过它允许具体执行的时候能够有些灵活的变动。

ClassLoader加载原理

下面贴下jdk关于类加载的源码,上述四种类加载器中CustomClassLoader是用户自定义的,BootStrapClassLoaderjvm创建的,就不展示了;这里展示下AppClassLoaderExtClassLoader的启动过程,前面介绍过,AppClassLoaderExtClassLoader都是在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设为AppClassLoaderparent 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