Java虚拟机 ——类加载器

首发于公众号: DSGtalk1989

我们通过第一章节粗略的过了一下虚拟机将java代码变成可被机器阅读的机器码过程。

整个过程中最最重要的三个机制分别是

  • java源码编译机制
  • 类加载机制
  • 类执行机制

源码编译机制实际上是交给源码编译器解决,跟虚拟机没有直接关系。JVM处理的是类的加载和类的执行

一个类从加载到虚拟机内存到被卸载出内存,整个的生命周期要经历如下的7个阶段。分别是加载,验证,准备,解析,初始化,使用,卸载。验证,准备,解析这三个部分又被统称为链接

java类的生命周期

这里要说明一点,我们通常都会说到这个方法或者这个类,一般在使用的时候才会开始初始化,这就是java的运行时绑定,白话了说就是它到底是个什么要等到初始化了之后才知道,所以解析可能在初始化之前可能在初始化之后。这个点到为止。

2.1 类的加载器

我们先从类的加载(loading)开始,类的加载由ClassLoader及其子类完成,总共有如下四个ClassLoader

  • Bootstrap ClassLoader

    引导类加载器:加载最为基础,最为重要的类,比如jre/lib目录下的rt.jar文件,以及虚拟机参数-Xbootclasspath路径下的类。

  • Extension ClassLoader

    扩展类加载器:如其名,加载java平台中扩展功能的一些jar包,比如jre/lib目录下的其他jar文件,以及系统变量java.ext.dirs路径下的类。

  • Application ClassLoader

    应用类加载器:开始真正加载我们自己的类

  • Custom ClassLoader

    自定义类加载器:我们需要自己家在一些其他的类文件或者做一些其他操作。

维基百科是这么描述的:

JVM中有3个默认的类加载器:[[3]](https://zh.wikipedia.org/wiki/Java类加载器#cite_note-3)[4]

  1. 引导(Bootstrap)类加载器。由原生代码(如C语言)编写,不继承自java.lang.ClassLoader。负责加载核心Java库[5],存储在<JAVA_HOME>/jre/lib目录中。
  2. 扩展(Extensions)类加载器。用来在<JAVA_HOME>/jre/lib/ext,[6]java.ext.dirs中指明的目录中加载 Java的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。该类由sun.misc.Launcher$ExtClassLoader实现。
  3. Apps类加载器(也称系统类加载器)。根据 Java应用程序的类路径(java.class.path或CLASSPATH环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。该类由sun.misc.Launcher$AppClassLoader实现。

总结一下上面没有提到的:

引导类加载器是用原生代码编写的,(实际由C++编写),扩展类加载器由sun.misc.Launcher$ExtClassLoader实现,应用类加载器由sun.misc.Launcher$AppClassLoader实现。

如果只到这边为止,那网络上类似的会有很多,所以我们打开源码,探个究竟,要开始引起不适了,做好准备。

我们找到Launcher类,先来看扩展类加载器ExtClassLoader

static class ExtClassLoader extends URLClassLoader {
        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            final File[] var0 = getExtDirs();

            try {
                return (Launcher.ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction<Launcher.ExtClassLoader>() {
                    public Launcher.ExtClassLoader run() throws IOException {
                        int var1 = var0.length;

                        for(int var2 = 0; var2 < var1; ++var2) {
                            MetaIndex.registerDirectory(var0[var2]);
                        }

                        return new Launcher.ExtClassLoader(var0);
                    }
                });
            } catch (PrivilegedActionException var2) {
                throw (IOException)var2.getException();
            }
        }
            ...

        private static File[] getExtDirs() {
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }

            return var1;
        }
        ...
    }

通过静态方法,直接获取ExtClassLoader对象,过程中通过getExtDirs获取java.ext.dirs路径下的所有文件并进行注册registerDirectory

同样的,应用类加载器基本一样。

static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
                ...
}

获取java.class.path下所有类。

其实你还能在源码中发现这样一行。

private static String bootClassPath = System.getProperty("sun.boot.class.path");

所以我们的前面三个ClassLoader分别会查找sun.boot.class.path java.ext.dirs java.class.path来加载相应类。

接着,我们写一个Demo 来验证看看什么类用什么加载器加载出来,是否与我们上面描述的一致。

public static void main(String[] args){
  ClassLoader c = ArrayTest.class.getClassLoader();
  System.out.println("ClassLoader is " + c.toString());
}

代码中的ArrayTest是一个我们自定义的类,打印的结果为

ClassLoader is sun.misc.Launcher$AppClassLoader@14dad5dc

没有问题,我们所有自定义的类走的加载器为AppClassLoader

好的,我们再来接着看,我们尝试着去拿一下intClassLoader

public static void main(String[] args){
    ClassLoader c = int.class.getClassLoader();
    System.out.println("ClassLoader is " + c.toString());
}

结果报错了

Exception in thread "main" java.lang.NullPointerException
    at salamanca.Vmtest.main(Vmtest.java:6)

什么意思?找不到int类的ClassLoader,那int是怎么加载出来的呀?

针对ArrayTest类的ClassLoader我们再加一些打印,来看下他的父类情况。

public static void main(String[] args){
        ClassLoader c = ArrayTest.class.getClassLoader();
        System.out.println("ClassLoader is " + c.toString());
        System.out.println("ClassLoader is " + c.getParent().toString());
        System.out.println("ClassLoader is " + c.getParent().getParent().toString());
}

打印结果为

ClassLoader is sun.misc.Launcher$AppClassLoader@14dad5dc
ClassLoader is sun.misc.Launcher$ExtClassLoader@2f0e140b
Exception in thread "main" java.lang.NullPointerException
    at salamanca.Vmtest.main(Vmtest.java:8)

也就是说AppClassLoader的父加载器是ExtClassLoaderExtClassLoader没有父加载器。这里要注意,我们说的父加载器并不是通常的继承,而只是parent。根据最开始的维基百科介绍,猜测一下,之所以ExtClassLoader没有父加载器,是因为Bootstrap ClassLoader是由native代码实现的,因此直接在java层面是拿不到的。

我们抱着这些问题再回过头去啃一啃ClassLoader的源码。首先找到parent拿的到底是啥

public final ClassLoader getParent() {
        if (parent == null)
            return null;
        ...
        return parent;
}

返回的是parent,一步一步找到它的定义和它的赋值。

private final ClassLoader parent;

private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        ...
}

protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
}

protected ClassLoader() {
       this(checkCreateClassLoader(), getSystemClassLoader());
}

即构造ClassLoader传入一个parent ClassLoader,分别去到AppClassLoader,和ExtClassLoader看下传的parent都是谁。首先他俩都继承自URLClassLoader->SecureClassLoader->ClassLoader

AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
}

public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
            SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this);
}

public URLClassLoader(URL[] urls, ClassLoader parent) {
        super(parent);
        ...
}

URLClassLoader(URL[] urls, ClassLoader parent,
                   AccessControlContext acc) {
        super(parent);
        ...
}

protected SecureClassLoader(ClassLoader parent) {
        super(parent);
        ...
}

是不是已经很明显了,AppClassLoader传的是var2ExtClassLoader传的就是null。找一下AppClassLoader定义的地方。

public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader", var10);
        }

        try {
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", var9);
        }
                ...
}

static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }
}

清楚了吧,所以AppClassLoaderparent就是ExtClassLoaderExtClassLoaderparentnull。解释了我们前面的打印空指针现象,但是跟我们理解的AppClassLoader -> ExtClassLoader -> Bootstrap ClassLoader有点不一致,Bootstrap ClassLoader无缘无故就消失了。

先就此打住,我们先来看一下大家一直听到的双亲委托机制,然后再回过头来看,就豁然开朗了。

双亲委托

大家了解的双亲委托应该都是这么讲的

子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类

双亲委托

简单来说就是以下的白话流程

  1. 来了一个Class加载需求,先去Custom Classlodaer看是否加载过,如果没有加载过就问AppClassloader是否加载过,如此循环直到Bootstrap ClassLoader
  2. 当中任意环节加载过就直接返回加载的字节码
  3. 如果直到Bootstrap ClassLoader发现都没有加载过,那么开始去自己的路径下面即sun.mic.boot.class找是否有这个类,找到就开始加载,如果没有找到就往子加载器抛,如此循环直到抛到Custom ClassLoader
  4. 如果Custom ClassLoader的路径中都没有出现这个类,那么就抛错ClassNotFoundException

那么结合代码的话,如何去理解上面说的双亲委托和加载流程呢?

准备好了么?一切的谜题就要解开了!

我们去到ClassLoaderloadClass方法,双亲委托与自上而下的加载一步了然。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        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. Class<?> c = findLoadedClass(name);第一步,看下是否在自己这边已经被加载过了

  2. if (c == null)如果自己这儿没有加载过

  3. try

    if (parent != null) {
           c = parent.loadClass(name, false);
    } else {
           c = findBootstrapClassOrNull(name);
    }
    

    有父加载器,就调父加载器的loadClass方法,递归。如归没有定义父加载器parent比如ExtClassLoader,就走findBootstrapClassOrNull清爽不清爽!我们先不急着兴奋,接着往下把流程看完

  4. 如果依然没有找到就调findClass方法去找。

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

    已经很明显了,这个很有可能就是我们说的自上而下的加载。

    /**
    * Finds the class with the specified <a href="#name">binary name</a>.
    * This method should be overridden by class loader implementations that
    * follow the delegation model for loading classes, and will be invoked by
    * the {@link #loadClass <tt>loadClass</tt>} method after checking the
    * parent class loader for the requested class.  The default implementation
    * throws a <tt>ClassNotFoundException</tt>.
    * ...
    */
    protected Class<?> findClass(String name) throws ClassNotFoundException {
             throw new ClassNotFoundException(name);
    }
    

    果不其然,这个方法就是去找寻具体的类,并且由其子类实现,我们最终可以在两个加载器的父类URLClassLoader中可以找到findClass的方法实现

    protected Class<?> findClass(final String name)
            throws ClassNotFoundException
        {
            ...
            String path = name.replace('.', '/').concat(".class");
            Resource res = ucp.getResource(path, false);
            ...
    }
    

    而这个ucp就是我们说的资源路径

    /* The search path for classes and resources */
    private final URLClassPath ucp;
    

    你只要略微的找一下就能找到在AppClassLoaderExtClassLoader中,分别传入了pathjava.ext.dirsjava.class.path

补充

有个技巧告诉大家,我们按照如下步骤操作IDE,便可以在控制台看到类加载的过程

  1. 首先找到IDEA的工具栏的Run -> Edit Configrations

    1
  2. VM options中添加配置-XX:+TraceClassLoading就可以跟踪打印类的初始化流程了。

    2
  3. 我们在结果中就能看到类的加载流程了

    3

加载器介绍到此为止。