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

加载器介绍到此为止。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349