Java 类的加载、链接与初始化 - TongtongLan/Java GitHub Wiki

类的加载、链接与初始化

加载

Java虚拟机没有进行强制约束何时开始类加载的时间

  • 加载步骤如下:
  1. 通过一个类的权限定名来获取定义此类的二进制字节流(注意:没有限制从何处获取二进制字节流),相对于其他阶段此阶段可控性最强(数组类除外),可以使用系统提供的引导类加载器,也可以使用用户自定义类加载器完成(即重写类加载器的 loadClass() 方法)

  2. 分析并将这些二进制数据流转换为方法区(Java JVM内存架构:方法区、堆,栈,本地方法栈,pc 寄存器)特定的数据结构(这些数据结构是与实现有关的,不同 JVM 有不同实现)。这里处理了部分检验,比如类文件的魔数的验证,检查文件是否过长或者过短,确定是否有父类???《深入理解虚拟机》中指出这部分是在验证阶段完成的(除了 Object 类,此类是由启动类加载器加载的,具体见Java Object类详解)。

  3. 在内存中创建对应类的 java.lang.Class 实例(并没有明确规定是在 java 堆中,对于 Hotspot 而言,Class 对象存放在方法区中),作为方法区这个类的各种数据的访问入口(注意,有了对应的 Class 实例,并不意味着这个类已经完成了加载链接!)。

  • 二进制字节流获取方式
  1. 从 zip,jar等归档文件中加载 .class 文件

  2. 通过网络下载 .class 文件

  3. 运行时计算生成,这种场景使用最多的是动态代理技术

  4. 由其他文件生成,例如 JSP 应用

  5. 从数据库中获取,有些中间件服务器可以选择把程序安装到数据库中完成程序代码在集群间的分发。

  • 数组类加载方式
  1. 如果组件类型是引用类型,递归采用上述类加载过程加载组件类型,数组将在加载该组件类型的类加载器的类名称空间上被标识(因为一个类必须由它自身与其加载器一起确定唯一性)

  2. 如果组件类型不是引用类型,JVM 会把数组标记为与引导类加载器关联。

数组类的可见性与它的组件类型的可见性一致,如果组件类型不是引用类型,数组类的可见性默认为 public

链接

链接的过程比加载过成复杂不少,这是实现 Java 的动态性的重要一步。分为三部分:验证,准备和解析。


验证(非常重要但不是必要的)

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

链接的第三部解析会把类中成员方法、成员变量、类和接口的符号引用替换为直接引用,而在这之前,需要检测被引用的类型正确性和接入属性是否正确(就是 public ,private 的的问题),诸如检查 final class 又没有被继承,检查静态变量的正确性等等。(注意到实际上有一部分验证过程已经在加载的过程中执行了。)

  • 大致可分为四个验证阶段
  1. 文件格式验证,验证字节流是否符合class 文件格式的规范,并且能被当前版本的虚拟机处理

  2. 元数据验证,对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求

  • 类是否具有父类
  • 是否继承了被final修饰的类
  • 如果类不是抽象类,是否实现了必须实现的所有方法
  • 类中方法、字段是否与父类产生矛盾(例如覆盖了父类final方法,不符合规范的重载等)
  1. 字节码验证,通过数据流和控制流分析,确定程序语义是合法的

  2. 符号引用验证,对类自身意外的信息进行匹配性校验

  • 通过权限定名是否能找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段
  • 符号引用中的类、字段、方法的访问性验证

准备

正式为类变量分配内存并设置类变量初始值的阶段,这些类变量都将在方法区中分配(注意:这里内存分配的仅包括类变量,而不包括实例变量,实例变量会在对象实例化时随着对象一起存储在 java 堆中)

对类的成员变量分配空间。虽然有初始值,但这个时候不会对他们进行初始化(因为这里不会执行任何 Java 代码,而对类变量的赋值putstatic指令是在程序被编译后,存放于类构造器<clinit>()方法中)。具体如下:

所有原始类型的值都为 0。如 float: 0f, int: 0, boolean: 0(注意 boolean 底层实现大多使用 int),引用类型则为 null。值得注意的是,JVM 可能会在这个时期给一些有助于程序运行效率提高的数据结构分配空间。比如方法表(类似与 C++中的虚函数表,参见另一篇博文方法的虚分派和方法表)。

此外需注意,如果类字段的字段属性表中存在 ConstantValue 属性(即编译常量),那在准备阶段变量值就会被初始化为 ConstantValue 所指定的值


解析

可选步骤,没有规定发生的具体时间,事实上 jvm 对这一步可能采用延迟解析(late resolution), 直到引用被真正使用.

为符号引用定位直接引用(如果符号引用先到常量池中寻找符号,再找先应的类型,无疑会耗费更多时间),完成内存结构的布局。主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用。

符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时无歧义的定位到目标即可。与虚拟机实现内存布局无关。

直接引用:可以是直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。与虚拟机实现的内存布局相关。

解析与之后的类初始化是不冲突的,并非一定要所有的解析结束以后才执行类的初始化。不同的 JVM 实现不同。详情见另一篇博文Java类加载的延迟初始化

初始化类

开发 Java 时,接触最多的是对象的初始化。实际上类也是有初始化的。相比对象初始化(参见博文Java类的实例化总结Java中创建对象的5种方式),类的初始化机制要简单不少。

Java:对象创建和初始化过程

Java对象初始化详解


  • 何时初始化

JVM 规范对加载和链接的时机有很大灵活性,但对于初始化阶段,虚拟机严格规定了有且仅有5种情况(类被主动使用(active use))必须立即对类进行初始化(加载、验证、准备需要在此之前开始):

  1. 遇到 new 、getstatic 、putstatic 或 invokestatic 这4条字节码指令时,如果没有进行过初始化,则需要先触发其初始化,即使用 new 关键字实例化对象、读取或者设置一个类的静态字段(编译常量除外,已在编译期把结果放入常量池的静态字段除外(详情见相关常量池和运行时常量池的知识),对于父类中某非常量静态变量的调用除外)、调用一个类的静态方法。

  2. 使用 java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行过初始化,需先触发初始化

  3. 当初始化一个类的时候,其父类还未初始化,需先出发其父类的初始化。

  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main 方法的类),虚拟机会先初始化这个主类

  5. 当使用动态语言支持时


  • 类的初始化分两步:
  1. 如果基类没有被初始化,初始化基类。

  2. 有类构造函数,则执行类构造函数。

类构造方法<clinit>()是由 Java 编译器完成的。JVM 把类成员变量的初始化和 static 代码块提取出,放到类构造器方法中。这个方法不能被一般的方法访问(注意,static final 成员变量不会在此执行初始化,它一般被编译器生成 constant 值)。同时,其中不会显示类所调用的基类的,因为 1 中已经执行了基类的初始化。类的初始化还必须注意线程安全的问题。

  • 初始化顺序是由下面的几条规则决定的:
  1. 首先初始化静态域是因为静态域是放在方法区和class对象在一起的。

  2. 由于类加载的时候,会向上查找基类,因为子类的初始化依赖于基类首先初始化。所以会首先发生“基类->子类"顺序的类加载,类加载过程中,顺便完成了静态域的初始化。

  3. 另外一条规则是初始化块和域的初始化按照声明的顺序进行。

  • 初始化顺序

对于静态变量、静态初始化块、变量、初始化块、构造器,它们的初始化顺序依次是(静态变量、静态初始化块)->(变量、初始化块)->构造器。

对于静态成员(static块可以看成普通的一个静态成员)和普通成员,其初始化顺序只与其在类定义中的顺序有关,和其他因素无关。

  • 对于继承情况下的初始化顺序:
  1. 继承体系的所有静态成员初始化(先父类,后子类)

  2. 父类初始化完成(普通成员的初始化-->构造函数的调用)

  3. 子类初始化(普通成员-->构造函数)


延伸

  • 一个实例变量在对象初始化的过程中会被赋值几次?

在本文的前面部分,我们提到过,JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。

如果我们在实例变量初始化器中对某个实例x变量做了初始化操作,那么这个时候,这个实例变量就被第二次赋值了。

如果我们在实例初始化器中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。

如果我们在类的构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。

也就是说,一个实例变量,在Java的对象初始化过程中,最多可以被初始化4次。

  • 为什么接口不能定义成员变量,而只能定义 final static 变量。

  1. 接口是不可实例化,它的所有元素都不必是实例(对象)层面的。static 满足了这一点。

  2. 如果接口的变量能被修改,那么一旦一个子类实现了这个接口,并修改了接口中的非 final 变量,而该子类的子类再次修改这个非 final 的变量后,造成的结果就是虽然实现了相同的接口,但接口中的变量值是不一样的。

可参考通过类字面常量解释接口常量为什么只能定义为static final,类加载过程---Thinking in java

  • Class对象存储在方法区还是堆中?

当加载一个类完成后,会在内存中实例化一个java.lang.Class类的对象,也就是该类的类对象。但是并没有明确规定必须在java堆中存放该类对象,对于HotSpot虚拟机而言,类对象存放在方法区里,但是新版本HotSpot也许会存放在java堆中。——详见《深入理解java虚拟机》

⚠️ **GitHub.com Fallback** ⚠️