Android 面试经验 - 类的加载机制

类的加载机制

参考资料:
Java 类加载机制(阿里面试题)
从经典面试题看java中类的加载机制
面试官:请你谈谈Java的类加载过程
深入理解Java类加载器(ClassLoader)

目录

  • 类的加载机制
    • 目录
    • 类加载器
      • 启动类加载器
      • 扩展类加载器
      • 应用程序类加载器
      • 自定义类加载器
    • 类加载机制
      • 全盘负责
      • 双亲委派模型
      • 缓存机制
    • 类加载器中的四个重要方法
      • loadClass(String name, boolean resolve)
      • findClass(String name)
      • defineClass(byte[] b, int off, int len)
      • resolveClass(Class≺?≻ c)
    • 类加载过程
      • 加载
      • 连接
        • 验证
        • 准备
        • 解析
      • 初始化
      • 流程图
    • 子类继承父类时的执行顺序

类加载器

类的加载是由类加载器完成的,类加载器包括:启动类加载器(BootStrap)、扩展类加载器(ExtClassLoader)、应用程序类加载器(AppClassLoader)和自定义类加载器(java.lang.ClassLoader的子类)。

启动类加载器

一般用本地代码实现,负责加载JVM基础核心类库,即 JAVA_HOME\lib 目录下的类。

扩展类加载器

继承自启动类加载器,加载 \lib\ext 下的类,或者被 java.ext.dirs 系统变量指定的类。

应用程序类加载器

继承自扩展类加载器,加载 ClassPath 中的类,或者系统变量 java.class.path 所指定的目录中记载类,是用户自定义加载器的默认父加载器。

自定义类加载器

继承自 ClassLoader 类。

为什么要自定义类加载器

一方面是由于java代码很容易被反编译,如果需要对自己的代码加密的话,可以对编译后的代码进行加密,然后再通过实现自己的自定义类加载器进行解密,最后再加载。

另一方面也有可能从非标准的来源加载代码,比如从网络来源,那就需要自己实现一个类加载器,从指定源进行加载。

类加载机制

全盘负责

当一个类加载器负责加载某个 Class 时,该 Class 所依赖的和引用的其他 Class 也将由该类加载器负责载入,除非显式指定另外一个类加载器来载入。

双亲委派模型

如果一个类加载器收到了 Class 加载的请求,它首先不会自己去尝试加载这个 Class ,而是把请求委托给父加载器去完成,依次向上。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的 Class 时,即无法完成该加载,子加载器才会尝试自己去加载该 Class 。

这样做的好处是:
1. 避免同一个类被多次加载
2. 安全,Java 核心 API 中定义的类不会被随意替换
3. 每个加载器只能加载自己范围内的类

缓存机制

所有加载过的 Class 都会被缓存,当程序中需要使用某个 Class 时,类加载器先从缓存区寻找该 Class ,只有当缓存区不存在时,系统才会去读取该 Class 对应的二进制数据,并将其转换成 Class 对象,存入缓存区。

这就是为什么修改了 Class 后,必须重启JVM,程序的修改才会生效。

类加载器中的四个重要方法

loadClass(String name, boolean resolve)

protected Class loadClass(String name, boolean resolve)
      throws ClassNotFoundException
  {
      synchronized (getClassLoadingLock(name)) {
          // 先从缓存查找该class对象,找到就不用重新加载
          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) {
                  // 如果都没有找到,则通过自定义实现的findClass去查找并加载
                  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;
      }
  }

流程:
缓存 -> 父类加载器 -> 没有父类 -> 启动类加载器 -> 自己的 findClass() 方法

findClass(String name)

由自己负责加载类的方法。

在自定义类加载器时,需要重写该方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的 Class 对象。

defineClass(byte[] b, int off, int len)

将 byte 字节流解析成 JVM 能够识别的 Class 对象。

resolveClass(Class≺?≻ c)

解析 Class 对象,即将字节码文件中的符号引用转换为直接引用。

符号引用与直接引用

符号引用:即一个字符串,但是这个字符串给出了一些能够唯一性识别一个方法,一个变量,一个类的相关信息。

直接引用:可以理解为一个内存地址,或者一个偏移量。

举个例子,现在调用方法 hello(),这个方法的地址是 1234567 ,那么 hello 就是符号引用,1234567 就是直接引用。

类加载过程

类加载分为三个步骤:加载,连接,初始化

Android 面试经验 - 类的加载机制_第1张图片

加载

根据一个类的全限定名(如 java.lang.String )来读取该类的二进制字节流,解析成 JVM 能够识别的 Class 对象。

连接

验证

确保 Class 文件的字节流中包含信息符合虚拟机要求,不会危害虚拟机的安全。

主要包括四种验证:文件格式验证,元数据验证,字节码验证,符号引用验证。

准备

为类的静态变量分配内存并且设置初始值,这里的初始值指的是不同类型的默认值,如 int 默认值为0,引用的默认值为 null。

而 final 修饰的静态常量,因为 final 在编译的时候就会分配了,所以此时的值为代码中设置的值。

注意

类的静态变量会分配在方法区中,而实例变量是随着对象一起分配到 Java 堆中。

解析

将常量池内的符号引用替换为直接引用。

初始化

将静态变量和静态方法块按顺序从上到下初始化,即为准备阶段的静态变量重新赋值,设置为代码中指定的值。

执行构造函数。

如果该类具有父类,先初始化父类。

流程图

Android 面试经验 - 类的加载机制_第2张图片

子类继承父类时的执行顺序

Android 面试经验 - 类的加载机制_第3张图片

你可能感兴趣的:(Android,面经)