类型的生命周期

1.类型加载、连接和初始化

Java虚拟机通过加载【loading】、链接【linking】和初始化【initialization】的过程,使得某个类型对当前正在取运行的程序可用。

  • 加载是查找具有特定名称的类或接口类型的二进制表示,并从该二进制表示中创建出类或接口的过程。
  • 链接是获取一个类或接口,并将其组合到Java虚拟机的运行时状态,以便可以执行它。
  • 类或接口的初始化包括执行类或接口的初始化方法< clinit >

链接分为三个子步骤:

  1. 验证【Verification 】:验证确保类型正确地形成并适合Java虚拟机使用。
  2. 准备【Preparation 】:准备工作涉及到分配类型所需的内存,例如任何类变量的内存。
  3. 解析【Resolution 】:解析是将常量池中的符号引用转换为直接引用的过程

具体的实现可能会延迟解析【Resolution 】步骤,直到正在运行的程序实际使用了某个符号引用。验证【Verification 】、准备【Preparation 】和(可选)解析【Resolution 】完成后,类型就可以进行初始化了。

Java虚拟机规范在类和接口加载【【loading】】和链接【linking】的时机上为实现提供了灵活性,但是严格定义了初始化的时间。任何JVM实现必须在每个类或接口首次主动使用时初始化,下面是6种主动使用的时机:

  1. 创建类的新实例时(通过new字节码指令,或者通过隐式的创建、反射、克隆或者反序列化)
  2. 调用类声明的静态方法(通过invokestatic字节码中指令)
  3. 由类或接口声明的静态字段的使用或赋值(通过getstatic字节码指令或者putstatic字节码指令),static final的字段不会被Javad的编译器优化为编译时常量,该常量的访问并不会触发定义类的初始化
  4. Java API中某些反射方法的调用,如类中Class的方法或Java.lang.reflect中的类的方法
  5. 类的子类的初始化(类的初始化需要其超类的初始化)。
  6. Java虚拟机的启动类(即包含main方法的那个类)

除了这里列出的6种主动使用途径之外,类型的所有其他使用途径都是被动使用,它们不会触发类型的初始化。

在首次主动使用时,类型必须初始化。然而,在初始化它之前,它必须被链接。在它被链接之前,它必须被加载。

类的初始化需要预先初始化它的超类,且递归。但是接口不同,只有某个接口所声明的非常量字段被使用时,该接口才会被初始化,而不会因为该接口的子接口或者子实现类被初始化而初始化。
因此,类的初始化要求其祖先类(不包括祖先接口)被初始化,而接口的初始化,并不要求其祖先接口被初始化。

2. 加载

加载过程包括三个基本活动。要加载类型,Java虚拟机必须:

  1. 根据给定类型的全限定名,生成表示该类型的二进制数据流
  2. 将二进制数据流解析为存储在方法区中的内部数据结构
  3. 在堆中创建该类型对应的java.lang.Class实例来表示该类型

Java虚拟机规范没有指定必须如何生成类型的二进制数据。为一个类型生成二进制数据的一些可能的方法是:

  • 从本地文件系统加载Java类文件
  • 从本地文件系统加载Java类文件
  • 从ZIP、JAR、CAB或其他归档文件中提取Java类文件
  • 从专用数据库中提取Java类文件
  • 动态地将Java源文件编译成class文件格式
  • 动态计算类型的类文件数据
  • 上述任何一种,但使用Java类文件以外的二进制文件格式

有了类型的二进制数据,Java虚拟机必须对其进行充分的处理,最终可以为其创建Class实例。虚拟机必须可以将二进制数据解析为依赖于实现的内部数据结构(见JVM虚拟机博客中,有关存储类数据的潜在内部数据结构的讨论)。

Class实例是加载【Loading】步骤的最终产物,它充当应用程序和内部数据结构之间的接口。要访问关于存储在内部数据结构中的类型的信息,程序将调用该类型的Class实例的对应方法。将类型的二进制数据解析为方法区中的内部数据结构,并在堆上实例化Class对象的过程称为创建【creating 】类型。

类型的加载时通过类加载器实现的~~

3. 连接

3.1 验证【Verification】

类型被加载【loading】后,就可以连接【linking】它了。连接【linking】它过程的第一步是验证【Verification】——确保类型符合Java语言的语义,并且不会破坏虚拟机的完整性。

验证并非严格的限制在加载【loading】之后,准备【Preparation】之前。在大多数Java虚拟机实现中,某些特定类型的检查一般都在特定的时机发生,比如:

  • 在装载【loading】阶段,虚拟机必须解析表示该类型的二进制数据,并构建出内部数据库解构。此时,必须执行某些检查,以确保解析的二进制数据的初始工作不会导致虚拟机崩溃。在这个二进制数据的解析阶段,虚拟机实现可能对该数据进行检查,以确保其具有预期的总体格式。比如魔术,验证文件是否太短或太长等等。尽管这些检查发生在加载【loading】期间,在链接【linking】的正式验证【Verification】阶段之前,但它们在逻辑上仍然是验证【Verification】阶段的一部分。检测被转载的类型是否有问题的整个过程都被归属于验证的范畴之下。
  • 加载【loading】过程中可能发生的另一个检查是确保除了Object之外的每个类都有一个超类。
  • 发生在验证【Verification】阶段之后的检查是对符号引用的验证。动态链接的过程包括查找由存储在常量池中的符号引用引用的类、接口、字段和方法,并将符号引用替换为直接引用。当虚拟机搜索符号引用的实体(如类型、字段或方法)时,它必须首先确保实体存在。如果虚拟机发现该实体存在,则必须进一步检查引用类型是否具有访问该实体的权限。这些存在性和访问权限的检查在逻辑上归属于验证【Verification】,但却最有可能发生在解析阶段。解析本身可以延迟到程序第一次使用某个符号引用时,因此这些检查甚至可以在初始化之后进行。

那么在正式的验证【Verification】阶段会检查什么呢?即在正式的验证【Verification】阶段之前没有被检查过的,且在之后也不会被检查的:

  • 检查final类不能拥有子类
  • 检查final的方法不能被覆写
  • 确保没有不兼容的方法声明(例如,在当前类型及其超类型之间出现了两个具有相同名称,且入参具有相同数量、顺序和类型,但返回类型不同的方法)
  • 检查常量池中的所有条目彼此是否一致(比如 CONSTANT_String_info条目的string_index 必须指向的是CONSTANT_Utf8_info条目 )
  • 检查常量池中包含的所有特殊字符串(类名,字段和方法名,字段描述符和方法描述符)是否复合格式
  • 检查字节码的完整性

3.3.2 准备【Preparation】

在准备【Preparation】阶段,Java虚拟机为类变量【Class Variables ,即静态变量】分配内存,并将它们设置为默认的初始值。在初始化阶段之前,不会将类变量初始化为真正的初始值(在准备阶段并不会执行任何Java代码)。在准备期间,Java虚拟机将类变量新分配的内存设置为由变量类型决定的默认值。
PS:虚拟机对boolean的支持很少,故在北部,它是以int类型实现的,默认值为0,亦FALSE。

在准备阶段,Java虚拟机实现还可能为某些特殊的数据结构分配内存,以提高正在运行的程序的性能。一个这种特殊的数据结构的例子是:方法表,它包含指向类中每个方法的指针,包括那些继承自其超类的方法。方法表允许在对象上调用继承的方法时,无需在调用时搜索超类。

3.3.3 解析【Resolution】

解析是从类型的常量池中定位类、接口、字段和方法的符号引用,并将这些符号引用替换为直接引用的过程。

4. 初始化

类或者接口只有在被初始化后,也就是为类变量赋予正确的初始值,才类或接口的首次主动使用做好了准备。

在Java代码中,通过类变量初始化语句或静态初始化语句,可以为类变量指定适当的初始值。

Java编译器会收集类型的所有类变量初始化语句和静态初始化语句,并将它们放入一个特殊的方法中。对于类,此方法称为类初始化方法;对于接口,则称为接口初始化方法。在类和接口的Java class文件中,这个方法被命名为"< clinit >",该方法无法被Java程序调用,它仅可以被虚拟机调用,专用于将类变量初始化为正确的初始化值。

类初始化
public class Initialization {

    // 类变量初始化语句
    public static String name1 = "piemon";

    public static String name2;

    // 静态初始化语句
    static {
        name2 = "anokata";
    }

    public static void main(String[] args) {
        System.out.println("echo");
    }
}

<clinit>字节码:
  static {};
    Code:
       0: ldc           #5                  // String piemon
       2: putstatic     #6                  // Field name1:Ljava/lang/String;
       5: ldc           #7                  // String anokata
       7: putstatic     #8                  // Field name2:Ljava/lang/String;
      10: return

类的初始化由两个步骤组成:

  1. 如果存在直接超类,且直接超类未初始化,那么闲初始化直接超类
  2. 如果类存在类初始化方法,那么执行该方法
接口初始化
public interface InitializationInterface {
    int month = 10;
    int day = (int)(Math.random() * 2.0);
}
<clinit>字节码:
public interface InitializationInterface {
  public static final int month;
  public static final int day;
  // 注意:该方法中,只存在对day变量的初始化字节码
  static {};
    Code:
       0: invokestatic  #1                  // Method java/lang/Math.random:()D
       3: ldc2_w        #2                  // double 2.0d
       6: dmul
       7: d2i
       8: putstatic     #4                  // Field day:I
      11: return
}
public interface InitializationInterface2 {
    int month = 10;
    int day = 16;	//day的声明可以在编译时解析为常量
}

public interface InitializationInterface2 {
  public static final int month;
  public static final int day;
}

接口中声明的字段,都是隐式由public static final修饰的,且必须使用字段初始化语句进行初始化。

如果接口存在任何一个无法在编译时解析为常量的字段,那么他必然存在< clinit >方法,如例子中的day字段。

PS:Java规范定义了常量表达式的涉猎操作符,比如+、*、/等等,这些操作符会在编译时直接计算。

接口的初始化比较特殊,只有一个步骤:

  1. 如果存在接口初始化方法,那么执行该方法

PS:子类的< clinit >并不会显式的调用父类的< clinit >方法。在虚拟机调用< clinit >方法之前,虚拟机必须保证父类的< clinit >已经被执行了。

Java虚拟必须保证多个线程初始化同一个类时,有且仅有一个线程会执行< clinit >方法,其他线程wait,当执行< clinit >的线程完成了初始化,他必须通知这些等待的线程。

主动使用和被动使用

Java虚拟机在首次主动使用某类型时时初始化它们。

使用一个非常量的静态字段时,只有当类或者接口中的确声明了这个字段时,才算做主动使用,比如:

  • 父类中声明的字段可能会被子类引用
  • 接口中声明的字段可能会被其实现类或者子接口引用
    对于子类、子接口和接口的实现类而言,这就是被动使用—使用它们并不会触发它们初始化。
//接口
public interface Face {
    String face = "face";
}
//父类
public class PClazz {

    static String pclazz = "pclazz";

    static {
        System.out.println("PClazz ...");
    }
}
// 子类、子实现
public class Clazz extends PClazz implements Face {

    static String clazz = "clazz";

    static {
        System.out.println("Clazz ...");
    }
}
// 测试类

测试程序:

    public static void main(String[] args) {
        System.out.println(Clazz.face);
        //loging
        //face
    }


    public static void main(String[] args) {
        System.out.println(Clazz.pclazz);
        //loging
        //PClazz ...
        //pclazz
    }

    public static void main(String[] args) {
        System.out.println(Clazz.clazz);
        //PClazz ...
        //Clazz ...
        //clazz
    }

如果一个字段是由static final修饰的,且使用一个编译时常量表达式初始化,那么使用这样的字段,就不是对声明该字段的类的主动使用。Java编译器会将这样的字段解析成对常量的本地拷贝(该常量存在于引用者类的常量池中或字节码流中,或二者兼有)

4.对象的生命周期

加载、链接和初始化类之后,就可以使用它了。程序可以访问它的静态字段,调用它的静态方法,或者创建它的实例。

4.1 类实例化

实例化类有4种显式途径:

  1. new
  2. 调用Class或者java.lang.reflect.Constructor对象的newInstance()方法
  3. 现有对象的clone
  4. 通过java.io.ObjectInputStream类的getIbject()方法反序列化

实例化类的隐式图途径:

  • 启动类的main(String[] args)的入参
  • 类装载器为类型创建Class实例
  • 当Java虚拟机加载了一个在其常量池中包含CONSTANT_String_info条目的类时,它可以实例化新的String对象来表示那些常量字符串字面量。将方法区中的CONSTANT_String_info项转换为堆上的字符串实例的过程是常量池解析过程的一部分。
  • 通过执行包含字符串连接操作符的表达式产生对象

当Java虚拟机隐式或显式地创建类的新实例时,它首先在堆上分配内存来容纳对象的实例变量。所有在对象的类及其超类中声明的变量,包括隐藏的实例变量,都需要分配内存。

一旦虚拟机为新对象准备好了堆内存,它就立即将实例变量初始化为默认的初始值。

一旦虚拟机为新对象分配了内存并将实例变量初始化为默认值,它就可以为实例变量赋值正确的初始值了。根据创建对象的方式,Java虚拟机使用三种技术之一来完成此任务:

  • 如果使用clone创建对象,虚拟机将被克隆对象的实例变量的值复制到新对象中。
  • 如果使用ObjectInputStream的readObject()反序列化对象,那么虚拟机从从输入流读取的值,来初始化对象的非瞬态实例变量。
  • 否则,虚拟机将调用对象上的实例初始化方法。注意,与类初始化方法区分开来。实例初始化方法将对象的实例变量初始化为其适当的初始值。

Java编译器为它编译的每个类至少生成一个实例初始化方法。在Java类文件中实例初始化方法名为“< init >”。对于类的源代码中的每个构造函数,Java编译器都会为其生成一个< init >()方法。如果类没有显式声明构造器,则编译器会主动生成一个默认的无参数构造函数,该构造函数只调用超类的无参数构造函数。

4.2 垃圾收集和对象终结

如果一个类声明了一个名为finalize()的返回void的方法,垃圾收集器会在释放这个实例所占的内存之前执行该方法一次。
finalize()是一个普通方法,可以被应用程序调用。但是垃圾收集器只会调用一次!!
如果终结方法调用后,对象引用又被激活,一段时间后又重新变得不被引用,那么垃圾收集器不会再第二次调用finalize()

你可能感兴趣的:(JVM,JVM)