类加载 (Class Loading)

  1. 加载(Loading) 是整个类加载(Class Loading) 的第一个阶段,在 loading 阶段, jvm 通过类的全限定名获取到定义此类的二进制字节流, 加载阶段可能还没有完全结束的时候,连接(linking)工作就可以开始了. ClassLoader 的 java doc
  2. 连接(Linking) 验证,准备,解析三个阶段称为连接
    1. 验证(Verification)
    2. 准备(Preparation)
    3. 解析(Resolution)
  3. 初始化(Initialization)

JVM SPEC 并没有规定并没有强制约束什么时候执行 Loading, 但是对于 Initialization, 明确规定有且仅有以下六种情况必须立马对类进行初始化(当然Loading 和 Linking 可能已经完成了, 或者先执行 Loading 和 Linking). 称为对类的主动使用

  1. 遇到 new, getstatic, putstatic, 或 invokestatic 这四个字节码指令的时候,如果类型没有初始化,则需要先进行初始化. 典型的 java 代码场景有:
    • 读取或设置一个类型的静态字段(在编译期以明确把结果放入当前类常量池的情况除外)
    • 调用一个类的静态方法的时候
    • 使用 new 关键字创建类型实例的时候
  2. 对类型进行反射操作的时候, 如果类还未被初始化
  3. 初始化一个类的时候,如果发现其父类还未初始化,会递归地初始化父类,直到顶层(接口除外, 会在下面证明)
  4. 虚拟机启动时,会初始化包含 main 方法的类
  5. 使用 JDK7 中新加入的动态语言支持的时候, 如果一个 MethodHandler 最后的解析结果为 REF_getstatic, REF_putstatic, REF_invokestatic, REF_invokespecial 四种类型的方法句柄,并且这个句柄对应的类型没有初始化时
  6. 如果一个接口中定义了默认方法, 这个接口的实现类初始化的时候

下面是几个典型的类的被动使用

  1. 通过子类使用父类的静态字段, 不会导致子类的初始化, 但是会加载(Loading)子类
  2. 通过数组定义来引用类, 不会初始化数组成员类, 但是会初始化一个 “[Lxxx.xxx.Clazz” 的类, 由 newarray 触发虚拟机自动生成的的直接继承 Object 的类, 这个类包装了数组元素的访问
  3. 编译阶段可以直接放入常量池的情况
  4. ClassLoader.loadClass 也不会导致类的初始化

JVM 自带了以下几种类加载器

  • 根/启动(Bootstrap)类加载器: 它没有父加载器. 负责加载虚拟机的核心类库. 包括存放在 JAVA_HOME/lib 目录, 或者被 -X:bootclasspath 参数指定的路径中存放的指定名称的类库(如 rt.jar, tool.jar 等), 名字不符合也不会加载
  • 扩展类加载器(Extension Class Loader): 负责加载 JAVA_HOME/lib/ext 目录中或者被 java.ext.dirs 系统变量所制定的路径中所有的类库. 特殊性,该类加载器只会加载 jar 包中的类, 如果仅仅把自定义 class 文件放在加载路径中,并不会加载,如果使用 jar cvf … 打成 jar 包之后,就可以加载了(jdk8)
  • 应用类加载器(Application Class Loader): 负责加载 classpath 上的所有类库

jdk8 环境下执行以下代码可以输出各个类加载器负责加载的路径, jdk15 已经输出为空了, 这可能是 jdk9 引入的模块化导致又有了新的加载类的方式

public class Loader {
    public static void main(String[] args) {
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));
    }
}

类加载器的双亲委派模型(Parents Delegation Model)

双亲委派模型要求除了顶层的启动类加载器外,所有的类加载器都应该有自己的父加载器, 如果一个类加载器收到了一个加载类的请求,它首先不会自己去加载这个类, 而是把这个请求委派给父加载器去完成,每一层的类加载器都会如此,只有当父类加载无法加载指定的类的时候,当前类加载器才会尝试加载这个类. 如果有一个类加载器能够成功加载某一个类, 那么这个类加载器被称为这个类的 定义类加载器, 所有能够成功返回 Class 对象引用的类加载器(包括定义类加载器)都被称为初始类加载器. 下面是 ClassLoader 类的 loadClass 源码, 可以看到这个模型的实现非常简单

protected Class<?> loadClass(String name, boolean 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 {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                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
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

双亲委派模型的意义

  • 可以确保 java 核心库的类型安全: 所有的 java 应用都至少会引用 Object 类, 在运行期,这个类会被加载到虚拟机中.如果这个加载过程可以由应用自定义加载器完成的, 那么很可能就会在 JVM 中存在属于多个命名空间的 Object 类, 而这些类之间是不兼容的. 借助双亲委派模型, 可以确保核心类都是由同一个加载器加载的
  • 可以确保 java 核心类不会被自定义的类替代

打破双亲委派机制有什么意义

​ 不同的类加载器可以为相同名称(binary name) 的类创建额外的命名空间. 相同名称的类可以并存在虚拟机中,只需要用不同的类加载器来加载它们即可.不同类加载器所加载的类型之间是不兼容的,这就相当于在 JVM 中创建了许多隔离的 java 类空间,这类技术在许多框架中得到了实际使用 如 Pandora OSGI 等

线程上下文类加载器(Thread Context ClassLoader)

​ 从 java1.2 引入的, 这个类加载器可以通过 Thread#setContestClassLoader 来设置. 如果创建线程的时候没有设置, 它会从父线程继承一个, 如果在应用程序的全局范围都没有设置的话, 它默认就是系统类加载器.为了解决启动类加载器不能加载 SPI 的 provider 类来出现的. 这样父 ClassLoader 可以使用当前线程的线程上下文类加载器加载的类, 这就改变了父 ClassLoader 不能使用子 ClassLoader 或是其他没有直接父子关系的 ClassLoader 加载的类的情况, 打破了双亲委派模型.

​ SPI 就是一个典型的使用 Thread Context ClassLoader 打破双亲委派模型来加载所需 SPI 服务代码, 这是一种父类加载器去请求子类加载器完成类加载的行为

类加载器的命名空间

  • 每个类加载器都有自己的命名空间, 命名空间由该加载器及其所有的父加载器所加载的类组成
  • 同一个命名空间中的类是相互可见的. 子加载器的命名空间包含了所有父加载器的命名空间, 所以,子加载器加载的类能够看到所有父加载器加载的类, 反之则不能. 如果两个加载器没有直接或间接的父子关系,则他们各自加载的类互相不可见
  • 在同一个命名空间中, 不会出现类的完整名字(包括包名)相同的两个类;在不同的命名空间中, 有可能会出现

类的卸载

​ 当某个类的类加载器以及其所有的实例都已经不再存活了, 该类会在 gc 的时候被卸载掉, 注意! 只有用户自定义的类加载器加载的类才有可能被卸载

类加载的细节

  1. 类加载并不需要等到某个类被"首次主动使用"的时候才去加载, jvm 可以选择是否提前加载某个类而不执行连接,初始化等操作. JVM 规范允许类加载器在预料到某个类将要被使用时就预先加载他,如果在预先加载的过程中遇到了 .class 文件缺失或存在错误, 类加载器必须在程序首次主动使用该类时才报告错误(LinkageError), 如果这个类一直没有被程序主动使用,那么在程序运行过程中类加载器就不会报告错误 ??? 这里还没有明确的证明

  2. 对于静态字段来说,只有直接定义了该字段的类才会被初始化,但是加载器可能会将相关类也同时加载(被动引用情形 1)

    public class PreLoad {
        public static void main(String[] args) {
            int c = PreLoadSub.a;
        }
    }
    
    class PreLoadParent {
        public static int a = 1;
        static {
            System.out.println("Parent: <cinit>");
        }
    }
    
    class PreLoadSub extends PreLoadParent {
        public static int b = 2;
        static {
            System.out.println("Sub: <cinit>");
        }
    }
    

    上面代码只会输出 Parent: <cinit> 如果使用-XX:+TraceClassLoading 开启 TraceClassLoading 会看到先 load 父类, 后 load 子类的过程

    [0.150s][info ][class,load] org.wm.loading.PreLoadParent

    [0.150s][info ][class,load] org.wm.loading.PreLoadSub

  3. 当 JVM 初始化一个类的时候,要求它的所有父类都已被初始化,但是这条规则并不适用于接口

    • 在初始化一个类时,并不会先初始化它所实现的接口.

    • 在初始化一个接口时,并不会先初始化它的父接口.

      public class LoadingInterface {
          public static void main(String[] args) {
              new ImplClass();    // 单独运行这段代码会输出 ImplClass <cinit>
              Thread sentinel = Child.sentinel;   // 单独运行这段代码会输出 Child interface <cinit>
          }
      }
      
      interface Parent {
          int a = 1;
          Thread sentinel = new Thread() {
              {
                  System.out.println("Parent interface <cinit>");
              }
          };
      }
      
      interface Child {
          int b = 2;
          Thread sentinel = new Thread() {
              {
                  System.out.println("Child interface <cinit>");
              }
          };
      }
      
      class ImplClass implements Parent {
          static {
              System.out.println("ImplClass <cinit>");
          }
      }
      
    • 例外就是类初始化的第六中情况,如果接口中定义了默认方法, 这个接口需要在实现类前初始化

      package org.wm.loading;
      
      public class LoadingInterface {
          public static void main(String[] args) {
              new ImplClass();    
            	// 运行这段代码会输出 Child interface <cinit>
      				// ImplClass <cinit>
          }
      }
      
      
      interface Child {
          int b = 2;
          Thread sentinel = new Thread() {
              {
                  System.out.println("Child interface <cinit>");
              }
          };
      
          default void defaultMethod() {
      
          }
      }
      
      class ImplClass implements Child {
          static {
              System.out.println("ImplClass <cinit>");
          }
      
          @Override
          public void defaultMethod() {
      
          }
      }
      
      

    因此,一个父接口并不会因为它的子接口或者它的实现类的初始化而初始化. 只有当程序首次使用该接口时(例如使用该接口中定义的非编译时常量时),才会导致接口的初始化

  4. 每个类都会尝试使用加载它的类加载器来加载它所依赖的类, 由此可以引出线程上下文类加载器

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值