Java类的装载、链接和初始化

时间:2023-01-02 17:51:56

加载(Loading)

按如下三步执行

  • 1.通过类的全名产生对应类的二进制数据流。(注意,如果没找到对应类文件,只有在类实际使用时才抛出错误。)
  • 2.分析并将这些二进制数据流转换为方法区(JVM 的架构:方法区、堆,栈,本地方法栈,pc 寄存器)特定的数据结构(这些数据结构是实现有关的,不同 JVM 有不同实现)。这里处理了部分检验,比如类文件的魔数的验证,检查文件是否过长或者过短,确定是否有父类(除了 Obecjt 类)。
  • 3.创建对应类的 java.lang.Class 实例(注意,有了对应的 Class 实例,并不意味着这个类已经完成了加载链链接!)。

链接(Linking)

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

  • 1.验证(verification)

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

  • 2.准备(preparation)

    对类的成员变量分配空间。虽然有初始值,但这个时候不会对他们进行初始化(因为这里不会执行任何 Java 代码)。具体如下:

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

  • 3.解析(Resolution)

    为类、接口、方法、成员变量的符号引用定位直接引用(如果符号引用先到常量池中寻找符号,再找先应的类型,无疑会耗费更多时间),完成内存结构的布局。

    这一步是可选的。可以在符号引用第一次被使用时完成,即所谓的延迟解析(late resolution)。但对用户而言,这一步永远是延迟解析的,即使运行时会执行 early resolution,但程序不会显示的在第一次判断出错误时抛出错误,而会在对应的类第一次主动使用的时候抛出错误!

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

初始化类(Initialization)

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

类的初始化也是延迟的,直到类第一次被主动使用(active use),JVM 才会初始化类。(参见博文《Java 类加载的延迟初始化》

类的初始化分两步:

  • 1.如果基类没有被初始化,初始化基类。
  • 2.有类构造函数,则执行类构造函数。

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

PS:

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

  • 1.接口是不可实例化,它的所有元素都不必是实例(对象)层面的。static 满足了这一点。
  • 2.如果接口的变量能被修改,那么一旦一个子类实现了这个接口,并修改了接口中的非 final 变量,而该子类的子类再次修改这个非 final 的变量后,造成的结果就是虽然实现了相同的接口,但接口中的变量值是不一样的。

综上述,static final 更适合于接口。

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