对象创建的过程

一个对象创建的过程主要分为类加载的过程和类实例化的过程。当一个类第一次使用的时候会进行一次类加载,并且只会进行一次类加载,除非类被虚拟机卸载掉后再次使用。而类实例化的过程则是每次实例化一个对象都会经历一次。

类加载的过程

我们书写的Java代码会被编译成.class文件,然后被虚拟机加载到内存。类加载经过的步骤很多,其中初始化这一步需要我们特别关注。

类的生命周期

类从被加载到内存开始,一直到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载7个阶段,其中验证、准备、解析3个部分又统称为连接。

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,而解析阶段是不确定的,大部分情况下都是在初始化之前,在某些情况下也可以在初始化之后再开始。

类初始化的时机

Java虚拟机规范并没有规定类加载的时机,但是对于初始化阶段,则规定了5种情况必须对类进行初始化,当然在初始化之前会进行加载、验证和准备阶段。

  • 遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则必须要先进行初始化。这几个指令最常见的Java代码场景是:使用new关键字新建一个对象的时候,读取或设置类静态变量的时候,以及调用类静态方法的时候。另外特别注意读取或设置类静态常量的时候,不会触发类的初始化。
  • 对类进行反射调用时,如果类没有进行过初始化,则必须要先进行初始化。
  • 当初始化一个类时,如果发现其父类没有进行过初始化话,则父类必须要先进行初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类,虚拟机会先初始化这个主类
  • 当使用JDK1.7的动态语言支持时,如果java.lang.invoke.MethodHandle实例最后解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则必须要先进行初始化。

下面我们说几个常见的不会触发初始化的场景

  • 通过子类引用父类的静态变量,只会触发父类的初始化,不会触发子类的初始化。
  • 定义类的数组,不会触发类的初始化。
  • 引用类的常量不会触发类的初始化,这是因为引用的常量会在编译阶段存入调用类的常量池中,本质上并没有直接引用到定义常量的类。

类加载过程

下面我们就来介绍一下类加载的过程,包括加载、验证、准备、解析、初始化5个阶段。

加载

加载阶段虚拟机主要做了3件事情:

  • 通过一个类的全限定名来获取一个类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。在HotSpot中,这个对象放在方法区而不是堆中。

验证

验证是连接阶段的第一步,这一步主要是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

验证阶段非常重要,这是因为加载进虚拟机的二进制字节流可以使用任何途径产生,如果不加以验证,很有可能会因为载入有害的字节流而导致系统崩溃。

验证阶段大致会完成以下4个阶段的验证动作:文件格式验证、元数据验证、字节码验证、符号引用验证。

准备

准备阶段是为类变量分配内存并设置类变量默认值的阶段,这些变量所使用的内存都将在方法区中分配。

这里需要说明两点:

  • 类变量是指类中被static修饰的变量,而不包括实例变量。
  • 默认值通常情况下是类变量数据类型的零值,代码里设置的初始值则是在初始化阶段赋予变量。如果类变量被static final修饰的话,则会被直接赋予代码里的初始值。

解析

解析阶段主要是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类。

初始化

类初始化阶段是类加载的最后一步,也是开始真正执行类中定义的Java代码的一步。初始化阶段主要是执行类构造器<clinit>()方法的过程。

这里需要强调的是类构造器方法并不是类的构造方法<init>()(类构造方法是在类实例化时调用),类构造器方法是编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序与它们在源码中出现的顺序一致。

类构造器并不需要显式地调用父类构造器,虚拟机会保证父类构造器先于子类构造器执行,也就是说父类里面的静态语句块和静态变量赋值优先于子类的静态语句块和静态变量赋值。

类实例化过程

类示例化的过程可以大致分为3步:

  • 分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量,在为这些实例变量分配内存的同时,也会为他们赋予默认值(零值)。
  • 为实例变量赋初始值、执行实例代码块中代码,赋初始值和执行实例代码块的顺序与它们在源码中的顺序一致
  • 执行构造方法

实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造方法中去,并且这些代码会被放在对父类构造方法的调用语句之后,构造方法本身的代码之前。

验证

下面我们就来写代码验证一下所学的知识,首先我们看单独一个类创建对象的过程:

public class SuperClass {
    public static String staticField = "静态变量";

    static {
        System.out.println(staticField);
        System.out.println("静态初始化块");
    }

    public String field = "变量";

    {
        System.out.println(field);
        System.out.println("初始化块");
    }

    public SuperClass() {
        System.out.println("构造方法");
    }

    public static void main(String[] args) {
        new SuperClass();
        new SuperClass();
    }
}

运行结果

静态变量
静态初始化块
变量
初始化块
构造器
变量
初始化块
构造方法

从结果中我们可以看出

  • 类加载的过程只会进行一次,而每次new一个对象,类实例化的过程都会触发一次。并且类加载的过程先于类实例化的过程。
  • 类实例化的过程是先为实例变量赋初始值、执行实例代码块中代码,然后是执行构造方法。

然后我们看下实例化一个子类的过程:

public class SuperClass {
    public static String staticField = "父类静态变量";

    static {
        System.out.println(staticField);
        System.out.println("父类静态初始化块");
    }

    public String field = "父类变量";

    {
        System.out.println(field);
        System.out.println("父类初始化块");
    }

    public SuperClass() {
        System.out.println("父类构造方法");
    }
}

public class SubClass extends SuperClass {
    public static String staticField = "子类静态变量";

    static {
        System.out.println(staticField);
        System.out.println("子类静态初始化块");
    }

    public String field = "子类变量";

    {
        System.out.println(field);
        System.out.println("子类初始化块");
    }

    public SubClass() {
        System.out.println("子类构造方法");
    }

    public static void main(String[] args) {
        new SubClass();
    }
}

运行结果是

父类静态变量
父类静态初始化块
子类静态变量
子类静态初始化块
父类变量
父类初始化块
父类构造方法
子类变量
子类初始化块
子类构造方法

从结果我们可以看出

  • 父类加载过程先于子类加载
  • 父类实例化过程先于子类实例化,但是注意这个过程并没有实例化父类对象


本作品采用
知识共享署名 4.0 国际许可协议进行许可,转载请注明原文链接


本文链接:https://schhx.github.io/2018/09/01/对象创建的过程/