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。

二、类加载器分类

JVM学习(二)类加载器_第1张图片
类加载器
  • 类加载器可以分为:

    • 启动类加载器(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学习(二)类加载器_第2张图片
    启动类加载器加载路径

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

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

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

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

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. 类加载器体系结构(双亲委派模型)

JVM学习(二)类加载器_第4张图片
类加载器体系结构

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

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

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

双亲委派模型
JVM学习(二)类加载器_第5张图片
双亲委派模型流程
JVM学习(二)类加载器_第6张图片
双亲委派模型加载流程

6. 类的加载方式

类的加载方式有三种:

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


      JVM学习(二)类加载器_第7张图片
      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()方法。
      JVM学习(二)类加载器_第8张图片
      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-自定义类加载器详解

你可能感兴趣的:(JVM学习(二)类加载器)