美文网首页java收录
JVM学习(二)类加载器

JVM学习(二)类加载器

作者: J先生有点儿屁 | 来源:发表于2018-08-02 16:39 被阅读7次

    目录


    一、类加载器

    还记得类加载机制吗?类加载机制的各阶段是加载、连接(验证、准备、解析)、初始化、使用、卸载。可参考上篇文章: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-自定义类加载器详解

    相关文章

      网友评论

        本文标题:JVM学习(二)类加载器

        本文链接:https://www.haomeiwen.com/subject/xysdvftx.html