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

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

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

Java虚拟机经过装载链接初始化一个Java类型,使该类型能够被正在运行的Java程序所使用。网络

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

总体流程以下:数据结构

image

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

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

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

除以上6种情形外,全部其余使用 Java 类型的方式都是被动使用,它们都不会致使 Java 类型的初始化。jvm

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

首次主动使用时初始化这个规则直接影响着类的装载、链接和初始化的机制。虚拟机实现能够自由选择装载、链接的时机。但不管如何,若是一个类型在它首次主动使用以前尚未被装载和链接的话,那它必须在此时被装载和链接,这样它才能被初始化。性能

装载

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

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

Java虚拟机并无说Java类型的二进制数据应该怎样产生。因此咱们能够想象这几种场景:ui

  • 本地文件系统装载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表示十、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 虚拟机的实现应该具备某种自动堆储存管理策略,而大部分采用垃圾收集器。当一个对象再也不被程序所引用了,虚拟机必须回收那部份内存。

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

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

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

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 类型和对象的生命周期就学习到这里。在类链接垃圾回收部分只是作了简单介绍,后面会补上。

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

相关文章
相关标签/搜索