JVM学习(二)类加载器

目录


一、类加载器

还记得类加载机制吗?类加载机制的各阶段是加载、连接(验证、准备、解析)、初始化、使用、卸载。可参考上篇文章:JVM学习(一):Java类的加载机制 里有详细说明。

1. 什么是类加载器?

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

2. 类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中祈祷的作用却远远不限于类加载阶段。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立啊Java虚拟机中的唯一性。每一个类加载器都拥有一个独立的类名称空间。
比较两个类是否“相等“,只有在这两个类是由同一个类加载器加载的前提下才由意义;否则,即使两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那么这两个类就必定不相等。
(这里指的“相等”,包括代表类的Class对象的equals()方法、isAssignableFrom()方法、isInstance()方法的返回结果,也包括关键字instanceof做对象所属关系判定情况。)

  • 代码演示:
    public class ClassLoaderTest {
        public static void main(String[] args) throws Exception {
            ClassLoader loader = new ClassLoader() {
                @Override
                public Class<?> loadClass(String name) throws ClassNotFoundException {
                    try {
                        String fileName = name.lastIndexOf("." + 1) + ".class";
                        InputStream inputStream = this.getClass().getResourceAsStream(fileName);
                        if (inputStream == null) {
                            return super.loadClass(name);
                        }
                        byte[] bytes = new byte[inputStream.available()];
                        inputStream.read(bytes);
                        return defineClass(name, bytes, 0, bytes.length);
                    } catch (IOException e) {
                        e.printStackTrace();
                        throw new ClassNotFoundException(name);
                    }
                }
            };
    
            Object obj = loader.loadClass("com.jx.Test1").newInstance();
            System.out.println(obj.getClass()); // 打印类名称
            System.out.println(obj instanceof com.jx.Test1); // 打印 比较obj对象是否是com.jx.Test1类
        }
    }
    
  • 运行结果:
      class com.jx.Test1
      false
    

从示例代码Object obj = loader.loadClass("com.jx.Test1").newInstance(); System.out.println(obj.getClass());打印结果是com.jx.Test1,说明通过自定义的类加载器 加载并实例的对象确实是Tes1的类;
但代码System.out.println(obj instanceof com.jx.Test1);运行输出的结果是false,这是因为在JVM虚拟机中存在另外一个类加载器加载了。
虽然都是来自同一个Class文件,但因为是两个独立的类加载器加载出来的类,在做对象所属类型检测时结果是false。

二、类加载器分类

类加载器
  • 类加载器可以分为:

    • 启动类加载器(Bootstrap ClassLoader)
    • 扩展类加载器(Extension ClassLoader)
    • 应用程序类加载器(Application ClassLoader)
    • 自定义类加载器(USer ClassLoader)
  • 他们的关系是:
    自定义类加载器的父类是应用程序类加载器;
    应用程序类加载器的父类是扩展类加载器;
    启动类加载器严格意义上不是扩展类加载器的父类,抽象维度可以理解为父类。

1. 启动类加载器(Bootstrap ClassLoader)

启动类加载器(Bootstrap ClassLoader) 是最顶层的类加载器,主要加载核心类库。

  • 加载路径\jdk\jre\lib下的rt.jar、resource.jar、charsets.jar和class等。
  • 启动类架子啊其是无法被Java程序直接引用的。
    Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层是由C++编写嵌入到JVM内核中;
    当JVM启动后 Bootstrap ClassLoader也随着启动,赋值加载完核心类库后,并构造Extension ClassLoader和Application ClassLoader。
    如图:
    启动类加载器加载路径

    另外,可以通过启动JVM时指定-Xbootclasspath路径来改变Bootstrap ClassLoader的加载目录。

2. 扩展类加载器(Extension ClassLoader)

扩展类加载器(Extension ClassLoader):这个类加载器由sun.misc.Luancher&ExtClassLoader实现。

  • 负责加载\jre\lib\ext目录下的jar包和class文件.
  • 或者由java.ext.dirs系统变量指定路径中的所有类库(如javax.开头的类),开发者可以直接使用扩展类加载器。
    如图:
    扩展类加载器加载路径

3. 应用程序类加载器(Application ClassLoader)

应用程序类加载器(Application ClassLoader):是由sun.misc.Launcher&ApplicationClassLoaer实现。

  • Application ClassLoader是负责加载用户类路径上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序没有自定义过自己的类加载器,一般情况下就是程序默认的类加载器。

4. 自定义类加载器(User ClassLoader)

**自定义类加载器(User ClassLoader)
**:一般是继承ClassLoader,重写findClass方法。
因为JVM自带的ClassLoader只会从本地文件系统加载标准的Java class文件,因此编写自定义类加载器可以做到:

    1. 在执行非自信代码之前,自动验证数字签名。
    1. 动态地创建符合用户特定需要的定制化构建类。
    1. 从特定的场所取得Java class,例如数据库和网络中。

5. 类加载器体系结构(双亲委派模型)

类加载器体系结构

关于类加载器的加载过程:

    1. 称为缓存查找环节。第一步是先检查类加载器中是否已经缓存加载了对应的类。 其中又分为:
    • ① 若存在自定义类加载器,则先检查自身缓存中是否存在;如果存在则取到。
    • ② 如果自定义缓存不存在,委托父类查找,也就是应用程序类加载器。
      应用程序类加载器同样也先检查缓存中是否存在,如果存在则取到。
    • ③ 如果应用缓存不存在,则委托它的父类,既是扩展类加载器。
      扩展类加载器同样也会先检查缓存中是否存在,如果存在则取到。
    • ④ 如果扩展类加载器缓存也不存在,则调用启动类加载器查找。
      启动类加载器也是先检查是否已经加载,如果加载,则取到。如果未加载,则进入加载环节。
    1. 加载环节。第二步,在所有类加载器通过缓存都找不到时,则进入类加载环节。类加载环节可分为:
    • ① 启动类加载器。启动类加载器在缓存找不到后,会根据它的路径范围jre\lib\rt.jar查找加载对应类。如果成功加载,则返回;如果不成功,则进入②。
    • ② 扩展类加载器。扩展类加载器在收到启动类加载器未成功的情况下,会根据它的路径访问jre\lib\ext\*.jar查找加载对应类。如果成功加载,则返回;如果不成功,则进入③。
    • ③ 应用程序类加载器。应用程序类加载器收到扩展类加载器不成功的情况下,会根据它的路劲访问ClassPath查找加载对应的类。如果成功加载,则返回;如果不成功,则进入④。
    • ④ 自定义类加载器。如果应用程序类加载器在收到应用程序类加载器不成功的情况下,会根据它自定义的路径访问查找加载对应的类。如果成功加载,则返回;如果不从,则抛出ClassNotFoundExcepiton异常。

上面的流程又可以称为是双亲委派模型。

双亲委派模型
双亲委派模型流程
双亲委派模型加载流程

6. 类的加载方式

类的加载方式有三种:

    1. 命令行启动应用的时候由JVM初始化加载。
      用一张图即可说明。请见下图:


      main方法JVM配置
    1. 通过Class.forName()方法动态加载。
    • ① 我们先看测试示例代码:
    public class TestClassLoader {
        public static void main(String[] args) throws ClassNotFoundException {
            Class.forName("com.jx.Test1");//直接通过Class.forName()来加载类
        }
    }
    
    • ② 接着跟进去查看Class.for()方法的实现。
    public static Class<?> forName(String className)
                throws ClassNotFoundException {
        Class<?> caller = Reflection.getCallerClass();
        return forName0(className, true, ClassLoader.getClassLoader(caller), caller); // 这里会调用ClassLoader.getClassLoader()方法获得该类的类加载器对象
    }
    
    • ③ 再跟进forName0()方法,是一个native方法。
        private static native Class<?> forName0(String name, boolean initialize,
                                            ClassLoader loader,
                                            Class<?> caller)
        throws ClassNotFoundException;
    
    • ④ 上面的native forName0()方法会调用类加载器的ClassLoader.loadclass()方法。
      Class.forName()方法调用栈

    通过上面调用栈会发现Class.forName()方法本质上最后会调用ClassLoader.loadClass()方法。

    1. 通过ClassLoader.loadClass()方法动态加载。
      直接上ClassLoader.loadClass()方法代码,代码的注释已经说明了很清楚了。
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }
    
    protected Class<?> loadClass(String name, boolean resolve) //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 {
                        // 如果父类不存在,则交给BootstrapClassLoader来加载。 什么时候父类不存在呢?其实就是ExtClassLoader不存在父类的情况。
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                    // 如果父类通过缓存+加载都无法找到,并抛出ClassNotFoundException异常时,则捕获异常但不处理。
                }
    
                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;
        }
    }
    

    代码中有几个关键调用需要注意:

    • Class<?> c = findLoadedClass(name)通过缓存查找判断是否存在该类。
      进一步查看该方法实现,又调用了native findLoadedClass0方法。
      protected final Class<?> findLoadedClass(String name) {
          if (!checkName(name))
              return null;
          return findLoadedClass0(name);
      }
      
      private native final Class<?> findLoadedClass0(String name);
      
    • ② 当parent != null时,c = parent.loadClass(name, false);。如果父类不为空,则委派给父类的loadClass()方法执行。
      当 parent == null是,c = findBootstrapClassOrNull(name);父类如果为空时,则委派给BootstrapClassLoader来查找。
      这里就是双亲委派模型出现了。
    • ③ 当在经过父类们缓存查找和加载后,仍然未找到该类,则本加载器会亲自进行查找c = findClass(name);。这个方法很关键。
      protected Class<?> findClass(String name) throws ClassNotFoundException {
              throw new ClassNotFoundException(name);
          }
      
      通常情况下,我们自定义的用户类加载器通过继承ClassLoader抽象类后,重写findClass()方法是比较的靠谱的。

    到这里已经把双亲委派模型讲解了,还顺带讲解了自定义类加载器。

三、ClassLoader代码解读-双亲委派模型

通过上面的《通过ClassLoader.loadClass()方法动态加载》已经将双亲委派模型已经详细讲解了。
部分补充请查看:
JVM学习(二)续1-ClassLoader代码解读-双亲委派模型

四、自定义类加载器详解

请参考另外一篇文章中有详细讲解。
JVM学习(二)续2-自定义类加载器详解

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

推荐阅读更多精彩内容