JVM 中,类的加载主要分为三个步骤:加载、链接和初始化。
加载
加载是指用类加载器查找字节流的过程,这个字节流可能是.class文件,也可能是代码生成或从网络获取的。
JVM 中默认有三个类加载器在工作。
首先是启动类加载器,只有它是由 C++ 实现的,Java 9 之前用来加载 JRE 的 lib 下 jar 包中的类,之后用来加载 java.base 下的关键模块。
其次是扩展类加载器,它的父加载器是启动类加载器,Java 9 之前用它加载通用的类,如 JRE 的 lib/ext 下 jar 包中的类,之后它被改名为平台类加载器,取代了一部分启动类加载器的工作。
最后是应用类加载器,它的父加载器是扩展类加载器,它被用来加载应用程序路径下的类。
除了默认的三个类加载器,还可以自定义加载器,比如加密.class文件,使用自定义的解密类加载器去加载这些文件。
类加载器使用双亲委派模型加载类。除了启动类加载器,每个类加载器都有父加载器,在加载类时会先尝试让父类加载器处理,如果父类加载器处理不了,自己才会处理。
链接
链接是指将加载好的类合并到 JVM 中,使之能够运行的过程。
链接的对象除了上面说的加载后的类,还有直接生成的数组类。
链接的步骤主要有三步:验证、准备和解析。验证是对加载的类进行检查,一般由编译器生成的字节码加载出的类不用在意这一步,但是通过字节码注入技术修改的类可能会在这一步出现验证失败;准备是为类的静态字段分配内存;加载一个类之前,编译器会生成符号引用,用来定位到字段和方法上,解析则是将符号引用解析为实际引用,这一步可能会触发被引用的类的加载。
初始化
初始化是为常量赋值和执行 <clinit> 方法的过程。这里涉及到两个知识点:一是基本类型和字符串如果被 final 修饰并直接赋值了,那么会被认为是常量,另一个是除了常量的赋值操作和静态代码块的代码,会被编译器置入生成的 <clinit> 方法中,该方法会通过加锁来确保只执行一次。
初始化的完成就标志着这个类被加载完成了。但是链接进虚拟机的类并不会被立即初始化,而是在条件达到时才触发,触发的条件主要有 8 种,分别是:虚拟机启动时初始化主类、类的实例被 new 指令创建时、类的静态方法被调用时、类的静态字段被访问时、类的子类被初始化时、定义 default 方法的接口的实现类被初始化时、类被反射调用时、类的方法 MethodHandle 指向并初次调用这个 MethodHandle 实例时。
类在被真正使用时再初始化可以显著地提高内存的使用效率,这也是懒汉式单例模式的意义所在。
总结
本文介绍了 Java 虚拟机加载类的过程,总的来说就是首先把字节流转化为类,然后让类能够执行,最后对类进行初始化这三步。其中,涉及到类加载器、双亲委派模型、符号引用、MethodHandle 等知识点,都是需要在后面的学习中注意的。
网友评论