深入Java虚拟机(三)Java类型的生命周期

539 阅读23分钟

上一篇简单记录了Java class文件的格式,它以标准的二进制形式来表现Java类型。本篇我们来看下当二进制的类型数据被导入到和Java虚拟机中时,到底会发生什么。我们以一个Java类型(类或接口)的生命周期(从进入虚拟机开始到最终退出)为例来讨论开始阶段的装载、连接和初始化,以及占Java类型生命周期绝大部分时间的对象实例化、垃圾收集和对象finalize,然后是Java类型生命周期的结束(从虚拟机中卸载)

生命周期的开始--类型装载、连接与初始化

Java虚拟机通过装载连接初始化一个Java类型,使该类型可以被正在运行的Java程序所使用。

  • 装载---就是把二进制形式的Java类型读入Java虚拟机中
  • 连接---就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接分为三个子步骤:
    • 验证---确保Java类型数据格式正确,并且适于Java虚拟机的使用。
    • 准备---负责为该类型分配所需要的内存,比如为它的类变量分配内存。
    • 解析---负责把常量池中的符号引用转换为直接引用。不过虚拟机的实现可以推迟这一步,它可以在程序真正使用某个符号引用时再去解析它。
  • 初始化---给类变量赋予正确的初始值。

整体流程如下:

image

如图所示,装载、连接和初始化这三个阶段必须按顺序进行。唯一例外的就是连接阶段的第三步(解析),它可以在初始化之后再进行。

在类和接口被装载和连接的时机上,Java虚拟机规范对具体实现提供了一定的灵活性。但是规范对于初始化的时机有着严格的规定。所有Java虚拟机实现必须在每个类或接口首次主动使用时初始化。下面6种情形符合主动使用的情形。

  • 当创建某个类的新实例时(通过执行new指令;或者不明确的创建、反射、克隆或者反序列化)
  • 当调用某个类的静态方法时(即在字节码中执行invokestatic指令输入时)。
  • 当使用某个类或接口的静态字段,或者对该字段赋值时(即在字节码中执行getstatic或putstatic指令时),用final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。
  • 当调用JavaAPI中的某些反射方法时,比如类Class中的方法或者java.lang.reflect包中类的方法。
  • 当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)
  • 当虚拟机启动时某个被标明为启动类的类(即含有main()方法的那个类)

除以上6种情形外,所有其他使用 Java 类型的方式都是被动使用,它们都不会导致 Java 类型的初始化。

第五条中任何一个类的初始化都要求它的超类在此之前完成初始化。然而对于接口来说,这条规则并不适用。只有在某个接口所声明的非final字段被使用时,该接口才会被初始化。

首次主动使用时初始化这个规则直接影响着类的装载、连接和初始化的机制。虚拟机实现可以自由选择装载、连接的时机。但无论如何,如果一个类型在它首次主动使用之前还没有被装载和连接的话,那它必须在此时被装载和连接,这样它才能被初始化。

装载

装载动作由三个基本动作组成,要装载一个类型,Java虚拟机必须:

  • 通过该类型的完全限定名,产生一个代表该类型的二进制数据流。
  • 将二进制数据流解析为方法区内的内部数据结构。
  • 创建一个表示该类型的java.lang.Class类的实例

Java虚拟机并没有说Java类型的二进制数据应该怎样产生。所以我们可以想象这几种场景:

  • 本地文件系统装载class文件
  • 网络下载class文件
  • 把一个源文件动态编译为class文件

有了二进制数据后,Java虚拟机必须对这些数据进行足够的处理,然后才能创建类java.lang.Class的实例对象。而装载步骤的最终产品就是这个Class类的实例对象,它成为Java程序与内部数据结构之间的接口。要访问关于该类型的信息,程序就要调用该类型对应的Class实例对象的方法。

前面讲过Java类型的装载要么由启动类装载器装载,要么由用户自定义的类装载器装载。而在装载过程中遇到问题,类装载器应该在程序首次使用时报告问题。如果这个类一直没有被程序主动使用,那么该类装载器不应该报告错误。

连接-第一阶段:验证

当类型被装载后,就准备进行连接了。连接过程的第一步是验证:确认类型符合Java语言的语义,并且不会危及虚拟机的完整性。

在验证上,不同虚拟机的实现可能不太一样。但虚拟机规范列出了虚拟机可以抛出的异常以及在何种条件下必须抛出它们。

验证阶段前的--验证

而在装载过程中,也可能会做以下几种数据检查(虽然这些检查在装载期间完成,在正式的连接验证阶段之前进行,但在逻辑上属于验证阶段。检查被装载类型是否有任何问题的过程都属于验证):

  • 在解析二进制流并转化为内部数据结构的时候,虚拟机要确保数据全部是预期格式。可能检查魔数,确保每一个部分都在正确的位置,是否拥有正确的长度等等。
  • 另一个可能在装载过程时进行的检查使,确保除了Object之外的每一个类都有一个超类。

验证阶段后的--验证

在大多数虚拟机的实现中,还有一种检查往往发生在正式的验证阶段之后,那就是符号引用的验证。

前面讲过动态连接的过程包括通过保存在常量池汇总的符号引用查找被引用的类、接口、字段以及方法,把符号引用替换为直接引用。

当虚拟机搜寻一个被符号引用的元素(类型、方法)时,它必须首先确认该元素存在。如果该元素存在,还要进一步检查引用类型是否有访问该元素的权限。这些存在性和访问权限的检查在逻辑上属于连接的第一阶段,但是往往在连接的第三阶段解析的时候发生。而解析自身也可能延迟到符号引用第一次被程序使用的时候,所以这些检查设置可能在初始化之后才进行。

正式的验证阶段

任何在此之前没有进行的检查以及在此之后不会被检查的项目都包含在内。

  • 检查 final 的类不能拥有子类
  • 检查 final 的方法不能被覆盖
  • 确保在类型和其父类之间没有不兼容的方法声明

请注意,当需要查看其他类型时,它只需要查看超类型。超类需要在子类初始化前被初始化,所以这些类应该已经被装载了。而对于接口的初始化来说,不需要父接口的初始化,但是当子接口被装载时,父接口需要被装载(它们不会被初始化,只是被装载了,有些虚拟机实现也可能会进行连接的操作)

  • 检查所有的常量池入口之间的一致性(比如,一个CONSTANT_String_info入口的 string_index项目必须是CONSTANT_Utf8_info入口的索引)
  • 检查常量池中所有的特殊字符串(类名、字段名和方法名、字段描述符和方法描述符)是否符合格式。
  • 检查字节码的完整性。

所有的Java虚拟机都必须设法为它们执行的每个方法检验字节码的完整性。比如,不能因为超出了方法末尾的跳转指令而导致虚拟机的崩溃,虚拟机必须在字节码验证的时候检查出这样的跳转指令是非法的,从而抛出一个错误。

虚拟机的实现并没有强求在正式的连接验证阶段进行字节码验证,所以虚拟机可以选择在执行每条语句的时候单独进行验证。然而Java虚拟机指令集设计的一个目标就是使得字节码流可以使用一个数据流分析器一次验证,而不用在程序执行时动态验证,对速度的提升有很大帮助。

连接-第二阶段:准备

随着Java虚拟机装载了一个类,并执行了一些它选择进行的验证后,类就可以进入准备阶段了。在准备阶段,Java 虚拟机为类变量分配内存,设置默认初始值。但在达到初始化之前,类变量都没有被设置为真正的初始值(代码里声明的)。准备阶段是不会执行 Java 代码的

在准备阶段,虚拟机给类变量的默认初始值如下表(有木有感觉跟 C 的默认数据类型很像)

类型 默认初始值
int 0
long 0L
short 0
char "\u0000"
byte 0
boolean false
reference null
float 0.0f
double 0.0d

Java 虚拟机通常把 boolean 实现为一个 int,会被默认赋值为0(对应 false)

在准备阶段,Java 虚拟机实现可能也为一些数据结构分配内存,目的是为了提高运行程序的性能。这种数据结构比如方法表,它包含指向类中每一个方法(包括从父类继承的方法)的指针。方法表可以在执行继承的方法时不需要搜索父类。

连接-第三阶段:解析

Java 类型经过验证和准备之后,就可以进入解析阶段了。解析的过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,并把这些符号引用转换成直接引用的过程。

本篇要从宏观角度看待生命周期,解析过程先简单描述下,后面做详细介绍

初始化

为了准备让一个类或接口被首次主动使用,最后一个步骤就是初始化。初始化就是为类变量赋予正确的初始值(就是代码里指定的数值)。

在 Java 代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化语句给出的。
类变量初始化语句(组成:=、表达式):

class ExampleA{
    static int size = (int) (3 * Math.random());
}

静态初始化语句(组成:static 代码块):

class ExampleB{
    static int size ;
    static {
        size = (int) (3 * Math.random());
    }
}

所有的类变量初始化语句和类型的静态初始化器都被 Java 编译器收集在一起,放到一个特殊的方法中。在类和接口的 class 文件中,这个方法被称为<clinit>。通常的 Java 程序方法是无法调用这个<clinit>方法的。这种方法只能被虚拟机调用,专门用来把类型的静态变量设置为它们的正确值。

初始化一个类包含两个步骤:

  • 如果类存在直接超类的话,并且超类还没有被初始化,就先初始化直接超类
  • 如果类存在一个类初始化方法,就执行此方法

而接口的初始化并不需要初始化它的父接口;如果接口存在一个初始化方法的话,就执行此方法。

<clinit>方法的代码并不会显式调用超类的<clinit>。在Java虚拟机调用类的<clinit>方法之前,它必须确认超类的<clinit>方法已经被执行了。

<clinit>方法

前面讲到Java 编译器把类变量初始化语句和静态初始化代码块都放到 class 文件的<clinit>方法中,顺序就按照在类或接口中声明的顺序。

如下类:

class ExampleB{
    static int width ;
    static int height = 4 * ExampleA.random();
    static {
        width = 9 * ExampleA.random() + 10;
    }
}

Java 编译器生成了如下<clinit>方法

 static <clinit>()V
   L0
    LINENUMBER 14 L0
    ICONST_4
    INVOKESTATIC hua/lee/jvm/ExampleA.random ()I
    IMUL
    PUTSTATIC hua/lee/jvm/ExampleB.height : I
   L1
    LINENUMBER 16 L1
    BIPUSH 9
    INVOKESTATIC hua/lee/jvm/ExampleA.random ()I
    IMUL
    BIPUSH 10
    IADD
    PUTSTATIC hua/lee/jvm/ExampleB.width : I
   L2
    LINENUMBER 17 L2
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0

并非所有的类编译后都存在<clinit>方法:

  • 如果类没有声明任何类变量,也没有静态初始化语句,那么它就不会有<clinit>方法。
  • 如果类声明了类变量,但是没有明确的进行初始化,也不会有<clinit>方法。
  • 如果类仅包含静态 final 变量的类变量初始化语句,而且初始化语句采用 编译时常量表达式,类也不会有<clinit>方法。

下面的类是不会执行<clinit>方法的。

class ExampleB{
    static final int angle = 10;
    static final int length = angle * 2;
}

ExampleB 声明了两个常量anglelength,并通过表达式赋予了初始值,这些表达式是编译时常量。编译器知道angle表示10、length表示20。所以在ExampleB被装载时,anglelength并没有作为类变量保存在方法区中,它们是常量,被编译器特殊处理了。

anglelength作为常量,Java 虚拟机在使用它们的所有类的常量池或者字节码流中直接存放的是它们表示的int 数值。比如,如果一个类A使用了 Example 的angle字段,在编译时虚拟机不会在类 A的常量池中保存一个指向Example类 angle 的符号引用,而是直接在类 A 的字节码流中嵌入一个值10。如果angle的常量值超过了 short 范围限制,比如 angle=40000,那么类会将它保存在常量池的 CONSTANT_Integer中,值为40000。

而对于接口来说,我们可以关注下面这个代码

interface ExampleI{
    int ketch = 9;
    int mus = (int) (Math.random()*10);
}

编译后的<clinit> 方法为

  static <clinit>()V
   L0
    LINENUMBER 22 L0
    INVOKESTATIC java/lang/Math.random ()D
    LDC 10.0
    DMUL
    D2I
    PUTSTATIC hua/lee/jvm/ExampleI.mus : I
    RETURN
    MAXSTACK = 4
    MAXLOCALS = 0

请注意,只有mus<clinit>初始化了。因为ketch字段被初始化为了一个编译时常量,被编译器特殊处理了。和类中的逻辑一样,对于其他引用到mus的类,编译器会保存指向这个字段的符号引用;而对于引用ketch的类,会在编译时替换为常量值。

主动使用被动使用

前面说过,Java 虚拟机在首次使用类型时初始化它们。只有6种活动被认为是主动使用:

  • 创建类的新实例
  • 调用类中声明的静态方法
  • 操作类或接口中声明的非 final 静态字段
  • 调用 API 种特定的反射方法
  • 初始化一个类的子类
  • 指定一个类作为Java 虚拟机的启动入口

使用一个非 final 的静态字段只有当类或者接口明确声明了这个字段时才是主动使用。比如,父类种声明的字段可能会被子类引用;接口中声明的字段可能会被实现者引用。对于子类、子接口和接口实现类来说,这就是被动使用。而被动调用并不会触发子类(调用字段的类)的初始化。

示例:

class NewParent{
    static int hoursOfSleep = (int) (Math.random() * 3);
    static{
        System.out.println("NewParent was initialized.");
    }
}

class NewKid extends NewParent{
    static int hoursOfCrying = 6+(int) (Math.random() * 2);
    static{
        System.out.println("NewKid was initialized.");
    }
}
public class Test {
    public static void main(String[] args) {
        int h = NewKid.hoursOfSleep;
        System.out.println(h);
    }
    static{
        System.out.println("MethodTest was initialized.");
    }
}

输出如下:

MethodTest was initialized.
NewParent was initialized.
2

从log看,执行Testmain方法只会导致TestNewParent的初始化,NewKid没有被初始化。

如果一个字段既是static 的又是final的,并且使用一个编译时常量表达式初始化,使用这样的字段,也不是对声明该字段类的主动使用。

看下面的代码:

interface Angry {
    String greeting = "Grrrr!";//常量表达式
    int angerLevel = Dog.getAngerLevel();//非常量表达式,会打包进<clinit>方法,而且调用DOG的静态方法,主动使用。
}
class Dog{
    static final String greeting = "woof woof world";
    static{
        System.out.println("Dog was initialized");
    }
    /**
     * 静态方法
     */
    static int getAngerLevel(){
        System.out.println("Angry was initialized");
        return 1;
    }
}
class Example01{
    public static void main(String[] args) {
        System.out.println(Angry.greeting);
        System.out.println(Dog.greeting);
    }
    static{
        System.out.println("Example was initialized");
    }
}
class Example02{
    public static void main(String[] args) {
        System.out.println(Angry.angerLevel);
    }
    static{
        System.out.println("Example was initialized");
    }
}

Example01 只是引用了静态常量(常量表达式形式的初始化)Angry.greetingDog.greeting,所以编译时已经替换为了实际数值,不属于主动使用,不会初始化对应的类。
Example01 的输出:

Example01 was initialized
Grrrr!
woof woof world

Example02引用了Angry.angerLevel,虽然是静态常量,但是是通过方法调用的方式Dog.getAngerLevel()初始化数值,属于主动使用Angry。而Angry调用了Dog的静态方法getAngerLevel(),属于主动使用Dog
Example02 的输出:

Example02 was initialized
Dog was initialized
Angry was initialized
1
woof woof world

类型的实例--对象的创建和终结

一旦一个类被装载连接初始化。它就随时可用了。程序访问它的静态字段,调用它的静态方法,或者创建它的实例。

对象的创建--类实例化

在Java程序中,类可以被明确或者隐含的实例化。明确的实例化一个类有四种途径:

  • 明确使用new操作符
  • 调用Classjava.lang.reflect.Constructor对象的newInstance()
  • 调用现有对象的clone()方法
  • 通过java.io.ObjectInputStream类的getObject()方法反序列化

另外存在下面几种隐含实例化类的方式:

  • 对于Java虚拟机装载的每一个类,它会暗中实例化一个Class对象来代表这个类型。
  • 当Java虚拟机装载了在常量池中包含CONSTANT_String_info入口的类的时候,它会创建新的String对象的实例来表示这些常量字符串。
  • 通过执行包含字符串连接操作符的表达式产生对象。

如下代码示例:

class Example{
    public static void main(String[] args) {
        if (args.length != 2){
            System.out.println("illegal args");
            return;
        }
        System.out.println(args[0] + args[1]);
    }
    static{
        System.out.println("Example was initialized");
    }
}

字节码内容:

  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 20 L0
    ALOAD 0
    ARRAYLENGTH
    ICONST_2
    IF_ICMPEQ L1
   L2
    LINENUMBER 21 L2
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "illegal args"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L3
    LINENUMBER 22 L3
    RETURN
   L1
    LINENUMBER 24 L1
   FRAME SAME
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    ALOAD 0
    ICONST_0
    AALOAD
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    ICONST_1
    AALOAD
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 25 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    MAXSTACK = 4
    MAXLOCALS = 1

请关注 args[0] + args[1],编译器会创建StringBuilder实例,通过 StringBuilder.append 连接,再通过 StringBuilder.toString 转成 String 对象

当 Java 虚拟机创建一个类的新实例时,不管明确的还是隐含的,首先都需要在堆中为保存对象的实例变量分配内存,包括在当前类和它的超类中所声明的变量。一旦虚拟机为新的对象准备好了堆内存,它立即把实例变量初始化为默认初始值(虚拟机默认值)。随后才会为实例变量赋予正确的初始值(码农期望的)。

Java 编译器为它编译的每一个类都至少生成一个实例初始化方法(构造函数)。在 Java class文件种,实例初始化方法被称为<init>。针对源码中每一个类的构造方法,Java 编译器都产生一个对应的<init>方法。如果类没有明确声明构造方法,编译器默认产生一个无参构造方法。

构造方法代码示例:

class ExampleCons{
    private int width = 3;

    public ExampleCons() {
        this(1);
        System.out.println("ExampleCons(),width = " + width);
    }

    public ExampleCons(int width) {
        this.width = width;
        System.out.println("ExampleCons(int),width = " + width);
    }
    public ExampleCons(String msg) {
        super();
        System.out.println("ExampleCons(String),width = " + width);
        System.out.println(msg);
    }

    public static void main(String[] args) {
        String msg = "Test Constructor MSG";
        ExampleCons one = new ExampleCons();
        ExampleCons two = new ExampleCons(2);
        ExampleCons three = new ExampleCons(msg);
    }
}

控制台输出:

ExampleCons(int),width = 1
ExampleCons(),width = 1
ExampleCons(int),width = 2
ExampleCons(String),width = 3
Test Constructor MSG

而从字节码上看,所有的<init>方法都默认执行了父类的无参构造方法:

// class version 52.0 (52)
// access flags 0x20
class hua/lee/jvm/ExampleCons {

  // compiled from: Angry.java

  // access flags 0x2
  private I width

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 39 L0
    ALOAD 0
    ICONST_1
    INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> (I)V
   L1
    LINENUMBER 40 L1
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "ExampleCons(),width = "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD hua/lee/jvm/ExampleCons.width : I
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L2
    LINENUMBER 41 L2
    RETURN
   L3
    LOCALVARIABLE this Lhua/lee/jvm/ExampleCons; L0 L3 0
    MAXSTACK = 3
    MAXLOCALS = 1

  // access flags 0x1
  public <init>(I)V
   L0
    LINENUMBER 43 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 36 L1
    ALOAD 0
    ICONST_3
    PUTFIELD hua/lee/jvm/ExampleCons.width : I
   L2
    LINENUMBER 44 L2
    ALOAD 0
    ILOAD 1
    PUTFIELD hua/lee/jvm/ExampleCons.width : I
   L3
    LINENUMBER 45 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "ExampleCons(int),width = "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ILOAD 1
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 46 L4
    RETURN
   L5
    LOCALVARIABLE this Lhua/lee/jvm/ExampleCons; L0 L5 0
    LOCALVARIABLE width I L0 L5 1
    MAXSTACK = 3
    MAXLOCALS = 2

  // access flags 0x1
  public <init>(Ljava/lang/String;)V
   L0
    LINENUMBER 48 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 36 L1
    ALOAD 0
    ICONST_3
    PUTFIELD hua/lee/jvm/ExampleCons.width : I
   L2
    LINENUMBER 49 L2
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "ExampleCons(String),width = "
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD hua/lee/jvm/ExampleCons.width : I
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L3
    LINENUMBER 50 L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 1
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER 51 L4
    RETURN
   L5
    LOCALVARIABLE this Lhua/lee/jvm/ExampleCons; L0 L5 0
    LOCALVARIABLE msg Ljava/lang/String; L0 L5 1
    MAXSTACK = 3
    MAXLOCALS = 2

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 54 L0
    LDC "Test Constructor MSG"
    ASTORE 1
   L1
    LINENUMBER 55 L1
    NEW hua/lee/jvm/ExampleCons
    DUP
    INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> ()V
    ASTORE 2
   L2
    LINENUMBER 56 L2
    NEW hua/lee/jvm/ExampleCons
    DUP
    ICONST_2
    INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> (I)V
    ASTORE 3
   L3
    LINENUMBER 57 L3
    NEW hua/lee/jvm/ExampleCons
    DUP
    ALOAD 1
    INVOKESPECIAL hua/lee/jvm/ExampleCons.<init> (Ljava/lang/String;)V
    ASTORE 4
   L4
    LINENUMBER 58 L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
    LOCALVARIABLE msg Ljava/lang/String; L1 L5 1
    LOCALVARIABLE one Lhua/lee/jvm/ExampleCons; L2 L5 2
    LOCALVARIABLE two Lhua/lee/jvm/ExampleCons; L3 L5 3
    LOCALVARIABLE three Lhua/lee/jvm/ExampleCons; L4 L5 4
    MAXSTACK = 3
    MAXLOCALS = 5
}

对象的终结--垃圾收集

对于Java程序来说,程序可以明确或隐含的为对象分配内存,但是不能明确的释放内存。我们前面讲过,Java 虚拟机的实现应该具有某种自动堆储存管理策略,而大部分采用垃圾收集器。当一个对象不再被程序所引用了,虚拟机必须回收那部分内存。

如果类声明了一个finalize()的方法,垃圾回收器会在释放这个实例占据的内存空间前执行这个方法。而垃圾收集器针对一个对象只会调用一次finalize()。如果执行finalize()期间对象被重新引用(复活了),随后又不被引用,垃圾收集器也不会再次执行finalize()方法。

垃圾收集器篇幅较长,本篇仍是以生命周期为主线,后面详细记录

类型生命周期的结束--卸载类型

Java 类的生命周期和对象的生命周期很像。

  • 对象:虚拟机创建并初始化对象,是程序可使用对象,然后在对象变得不再被引用时可选地进行垃圾收集。
  • 类:虚拟机装载、连接、初始化类,是程序能使用类,当程序不再引用这些类时可选地卸载它们。

类的垃圾收集和卸载之所以在虚拟机种很重要,是因为 Java 程序可以在运行时通过用户自定义的类装载器装载类型来动态地扩展程序。多有被装载嗯类型都在方法区占据内存空间。如果Java 程序持续通过用户自定义的类装载器装载类型,方法区的内存足迹就会不断增长。如果某些动态装载的类型只是临时需要,当他们不再被引用之后,占据的空间可以通过卸载类型而释放。

使用启动类装载器装载的类型永远是可触及的,所以永远不会被卸载。只有使用用户自定义的类装载器装载的类型才会变成不可触及的。
判断动态装载的类型的 Class 实例是否可以触及有两种方式:

  • 如果程序保持对 Class 实例的明确引用,它就是可触及的
  • 如果堆中还存在一个可触及对象,在方法区中它的类型数据指向一个 Class 实例,那么这个 Class 实例就是可触及的。

对于可触及,可以看下面这个类,Java 虚拟机需要建立起一个完整的引用(触及)链。

class MyThread extends Thread implements Cloneable{
}

引用关系图形化描述如下:

image

从可触及的MyThread对象指针开始,垃圾收集器跟随一个指向MyThread类型数据的指针,它找到了:

  • 一个指向堆中的MyThread的 Class 类的实例引用。
  • 一个指向MyThread的实现的接口 Cloneable 的类型数据指针。
  • 一个指向MyThread的直接超类Thread 的类型数据的指针。

从 Cloneable 的类型数据开始,垃圾收集找到了:

  • 一个指向堆中Cloneable的 Class 实例的引用。

从 Thread 的类型数据开始,垃圾收集器找到了:

  • 一个指向堆中的 Thread 的 Class 类的实例引用。
  • 一个指向 Thread 直接超类的 Object 的类型数据的指针。
  • 一个指向 Thread 的实现接口 Runnable 的类型数据的指针。

从 Runnable 的类型数据中,垃圾收集器找到了:

  • 一个指向堆中的 Runnable 的 Class 类实例的引用。

从 Object 的类型数据中,垃圾收集器找到了:

  • 一个指向堆中的 Object 的 Class 类实例的引用。

可以看到从一个 MyThread 对象开始,垃圾收集器可以触及和 MyThread 有关的所有信息。 PS:设计真滴是优秀啊

结束

Java 类型和对象的生命周期就学习到这里。在类连接垃圾回收部分只是做了简单介绍,后面会补上。

下面一篇详细介绍类的连接过程-连接模型