一、 类文件加载

  • 创建类加载器,类加载器从类路径根据需要加载的类的全限定名找到类文件.class,加载类文件,读取类文件字节码流,然后根据类文件的标准格式解析类文件内容,如最开头是魔数,然后是主次版本号等,根据静态常量池创建动态常量池,常量池包含的内容如下:
    • 字符串常量,符号引用等,所有字符串常量,类名,接口名,方法名,字段名(符号信息)和其他字符信息。虚拟机指令是依赖符号引用,而不是直接引用来设计的,即指令的执行没有依赖类,接口,实例等的运行时信息,而是依赖于常量池中的符号信息,如类名,方法名等,在运行之前需要将符号引用转为直接引用,即实际的运行时内存地址。
  • 除此之外还包含解析类实现的接口信息,字段信息,方法信息,初始化静态域。按照类文件标准格式解析出这些内容之后,创建类对象instanceKlass并保存到方法区中。
  • 在类对象instanceKlass中使用vtalbe表来存放虚方法和使用itables表来存放接口。
  • 具体过程如下:
    在这里插入图片描述

二、 链接

  • 类文件的加载过程只是从文件系统中读取类文件,然后根据类文件内容的标准格式来解析类文件内容,如字符串常量,对其他类的符号引用字符串,类实现的接口信息,包含的字段,方法等信息。
  • 但是类的主要功能是通过定义一系列方法来提供不同的功能,类文件加载过程只是从类文件解析出了这些方法,创建了对应的方法引用放在类对象instanceKlass的方法表里,但是并没有检查方法是否正常定义了,即是否存在语法问题。
  • 所以为了保证每个方法在被调用时,不存在因为无效变量,无效符号引用等而执行出错,如方法内使用的变量是否初始化,是否赋予了正确类型的值等,以及在方法执行时能够访问到所引用的其他类从而进行调用,所以在链接阶段的主要工作用于对类的方法进行这些检查和通过符号引用关联到直接引用来对其他类的方法进行调用,保证方法在运行时能够正常被调用。
1. 验证
  • 验证阶段包括对类,接口,方法的二进制字节码进行检查,逐一检查字节码的合法性。
  • 对方法的验证逐一包括:方法的访问控制,参数和静态类型检查,变量是否初始化和赋予了正确类型的值,局部变量表检查等。
2. 准备
  • 准备阶段主要是对类的静态变量进行内存分配和内存初始化,即是对内存赋予初始值,而不是根据类文件字节码的初始化语句进行初始化。如char为’\0000’,byte为0,boolean为0,float为0.0f,double为0.0d,不会进行实际的初始化,如某个静态变量static float num = 1.0f,在这个阶段的值还是0.0f,这是根据float类型的字节数分配了4个字节的内存空间。
3. 解析
  • 解析是JVM加载类阶段的一个可选过程,即也可以延迟到实际调用时来执行解析,即将符号引用解析为直接引用,Hotspot就是延迟解析的。
解析的定义
  • 每个类中会关联和引用其他类,这样在运行时调用这些类的方法。在类文件中是通过符号引用来记录当前类需要关联和引用的其他类,而符号引用是存放在类文件的常量池的字符串。
  • 在加载类文件过程中,对于类文件内部的常量池(静态常量池)会在运行时数据区(堆)中创建对应的运行时常量池,其中字符串类型的符号引用以字符串对象的形式存放在该运行时常量池中。
  • 如果只是字符串形式的符号引用,则无法与其他类进行关联,故在加载好类文件建立对应的类对象之后,需要将运行时常量池中的字符串对象形式的符号引用动态链接到实际的类对象,这个过程就是解析。
  • JVM并未规定这个过程发生的时间点,其中Hotspot虚拟机的符号引用到直接引用的类的链接过程发生在该类首次访问该符号引用时,即是基于懒链接实现的。
解析的内容
  • 解析主要是针对常量池的符号引用:类,接口,字段,类和接口的方法,进行解析。
  • 解析主要是将符号引用转为直接引用,即将表示被引用的类的字符串转为该被引用的类的运行时实际内存地址,从而可以在运行时进行调用。

三、初始化

  • 初始化阶段之前是必须是已经经历过了验证和准备,也可能是已经进行了解析(Hotspot是懒解析,还没进行解析,而是实际方法调用访问到该符号引用时才进行解析,定位到实际的内存地址)。
  • 由于应用一般是运行在多线程环境中的,故可能存在多个线程同时发起对该类的初始化操作,故需要进行加锁来保证类初始化的线程安全。加锁主要是对类对象instanceKlass对象进行加锁。
初始化的触发
  • 通过new创建类对象实例(在jvm层面是收到了new指令);
  • 调用类的静态方法和访问类的静态字段(在jvm层面是收到了getstatic, putstatic或invokestatic指令);
  • 通过反射加载和创建类对象,即调用类库的反射方法时,如Class类的newInstance和java.lang.reflect包;
  • 通过MethodHandler来调用类的方法时;
  • 初始化类的子类时,也会初始化父类。
初始化的内容
  1. 首先初始化使用final static修饰的类的常量和接口的常量,这些是编译期常数的域;
  2. 然后以文本顺序,即代码定义的顺序来初始化类的其他静态变量,调用类的静态初始化块。
  • 以上过程都是如果存在父类或接口则先初始化父类或接口,以此向上递归初始化父类的父类等,最后才初始化该类自身。如果父类还没进行加载验证准备,则需要先执行这些操作。
  • 初始化好之后,将该类对象instanceKlass放到JVM的方法区中(运行时常量池)。(JDK1.8之后字符串常量已经从方法区(运行时常量池)移到堆了)

总结

  • JVM进行类加载的以上三个过程具体如下:
    在这里插入图片描述
参考

《Hotspot实战》

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐