JVM类加载机制

JVM类加载机制

leo 557 2021-04-10

JVM类加载机制

JVM 类加载机制分为五个部分:加载,验证,准备,解析,初始化。

JVM类加载步骤

1. 加载

加载是类加载过程中的一个阶段, 这个阶段会在内存中生成一个代表这个类的 java.lang.Class 对象, 作为元空间这个类的各种数据的入口。注意这里不一定非得要从一个 Class 文件获取,这里既可以从 ZIP 包中读取(比如从 jar 包和 war 包中读取),也可以在运行时计算生成(动态代理),也可以由其它文件生成(比如将 JSP 文件转换成对应的 Class 类)。

2. 验证

这一阶段的主要目的是为了确保 Class 文件的字节流中包含的信息是否符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

3. 准备

准备阶段是正式为类变量分配内存并设置类变量的初始值阶段,即在方法区中分配这些变量所使用的内存空间。

4. 解析

解析是指虚拟机将常量池中的符号引用替换为直接引用的过程。

  • 符号引用:符号引用是class文件中CONSTANT_Class_info、CONSTANT_Field_info、CONSTANT_Method_info类型的常量
  • 直接引用:直接引用可以是指向目标的指针相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在内存中存在。

5. 初始化

初始化阶段是类加载最后一个阶段,前面的类加载阶段之后,除了在加载阶段可以自定义类加载器以外,其它操作都由 JVM 主导。到了初始阶段,才开始真正执行类中定义的 Java 程序代码。

初始化阶段是执行类构造器<clinit>方法的过程。<clinit>方法是由编译器自动收集类中的类变量的赋值操作静态语句块中的语句合并而成的。虚拟机会保证子<clinit>方法执行之前,父类的<clinit>方法已经执行完毕, 如果一个类中没有对静态变量赋值也没有静态语句块,那么编译器可以不为这个类生成<clinit>方法。

不会执行类初始化的情况

  • 调用父类静态成员
  • 定义对象数组
  • 常量在编译期间会存入调用类的常量池中,本质上并没有直接引用定义常量的类,不会触发定义常量所在的类的初始化
  • 通过类名获取Class对象
  • 通过 Class.forName 加载指定类时,如果指定参数 initialize 为 false 时,也不会触发类初始化,其实这个参数是告诉虚拟机,是否要对类进行初始化
  • 通过 ClassLoader 默认的 loadClass 方法

类加载器

虚拟机设计团队把加载动作放到 JVM 外部实现,以便让应用程序决定如何获取所需的类, JVM 提供了 3 种类加载器:

启动类加载器(Bootstrap ClassLoader)

负责加载 JAVA_HOME\lib目录中的, 或通过-Xbootclasspath 参数指定路径中的, 且被虚拟机认可(按文件名识别, 如 rt.jar) 的类。

扩展类加载器(Extension ClassLoader)

负责加载JAVA_HOME\lib\ext 目录中的,或通过java.ext.dirs 系统变量指定路径中的类库。

应用程序类加载器(Application ClassLoader)

负责加载用户路径(classpath)上的类库。

JVM 通过双亲委派模型进行类的加载, 当然我们也可以通过继承java.lang.ClassLoader实现自定义的类加载器。

类加载器结构图:

类加载器结构图

双亲委派模型

当一个类收到了类加载请求,他首先不会尝试自己去加载这个类,而是把这个请求委派给父类去完成,每一个层次类加载器都是如此,因此所有的加载请求都应该传送到启动类加载器中,只有当父类加载器反馈自己无法完成这个请求的时候(在它的加载路径下没有找到所需加载的Class), 子类加载器才会尝试自己去加载。

采用双亲委派的一个好处是比如加载位于 rt.jar 包中的类 java.lang.Object,不管是哪个加载器加载这个类,最终都是委托给顶层的启动类加载器进行加载,这样就保证了使用不同的类加载器最终得到的都是同样一个 Object 对象。

类加载流程