java 类的加载机制

类加载器

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

一般用本地代码实现,负责加载JVM基础核心类库,即 JAVA_HOMElib 目录下的类。
扩展类加载器

继承自启动类加载器,加载 libext 下的类,或者被 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 就是直接引用。

类加载过程

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


加载

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

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

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

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

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

注意

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

解析

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

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

执行构造函数。

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


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



---------------------
作者:路比船长
来源:CSDN
原文:https://blog.csdn.net/u013534071/article/details/80254247
版权声明:本文为博主原创文章,转载请附上博文链接!

原文地址:https://www.cnblogs.com/ldq2016/p/10442796.html