欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JVM--对象的实例化过程

程序员文章站 2022-05-24 21:08:45
...

Java对象创建时机

一个对象在可以被使用之前必须要被正确地实例化。在Java代码中,有很多行为可以引起对象的创建。下面对各种方式一一介绍。

使用new关键字创建对象

这是我们最常见的也是最简单的创建对象的方式,通过这种方式我们可以调用任意的构造函数(无参的和有参的)去创建对象。比如:

Student student = new Student();

 

使用Class类的newInstance方法(反射机制)

我们也可以通过Java的反射机制使用Class类的newInstance方法来创建对象,事实上,这个newInstance方法调用无参的构造器创建对象,比如:

 

Student student2 = (Student)Class.forName("Student类全限定名").newInstance();

或者:

Student stu = Student.class.newInstance();

使用Constructor类的newInstance方法(反射机制)

java.lang.relect.Constructor类里也有一个newInstance方法可以创建对象,该方法和Class类中的newInstance方法很像,但是相比之下,Constructor类的newInstance方法更加强大些,我们可以通过这个newInstance方法调用有参数的和私有的构造函数,比如:
 

public class Student {

    private int id;

    public Student(Integer id) {
        this.id = id;
    }
}

public class MainStart {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
       Constructor constructor =  Student.class.getDeclaredConstructor(String.class);
        //flag的值为 true 则指示反射的对象在使用时应该取消 Java 语言访问检查。
        constructor.setAccessible(true);
        Student student = constructor.newInstance(666);
    }
}

使用(反)序列化机制创建对象

当我们反序列化一个对象时,JVM会给我们创建一个单独的对象,在此过程中,JVM并不会调用任何构造函数。为了反序列化一个对象,需要让类实现Serializable接口。

Java 对象的创建过程

当一个对象被创建时,虚拟机就会为其分配内存来存放对象自己的实例变量及其从父类继承过来的实例变量(即使这些从超类继承过来的实例变量有可能被隐藏也会被分配空间)。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值(零值)。在内存分配完成之后,Java虚拟机就会开始对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构:

  •  实例变量初始化、实例代码块初始化
  •  构造函数初始化。

实例变量初始化

定义实例变量的同时,还可以直接对实例变量进行赋值或者使用实例代码块对其进行赋值。如果我们以这两种方式为实例变量进行初始化,那么它们将在构造函数执行之前完成这些初始化操作。实际上,如果我们对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。例如:

public class ObjectTest {

    private int n = 100;
    private int m ;

    public ObjectTest(){
        super();
        //实例变量直接赋值或者使用实例代码块赋值,会被编译器放到这里
        System.out.println(m);
    }

    { // 实例代码块
        m = n*2;
    }

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

特别需要注意的是,Java是按照编程顺序来执行实例变量初始化器实例初始化器中的代码的,并且不允许顺序靠前的实例代码块初始化在其后面定义的实例变量,即变量使用前必先定义

 构造函数初始化

每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成()方法,参数列表与Java语言书写的构造函数的参数列表相同。

Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。事实上,这一点是在构造函数中保证的:Java强制要求Object对象(Object是Java的顶层对象,没有超类)之外的所有对象构造函数的第一条语句必须是超类构造函数的调用语句或者是类中定义的其他的构造函数,如果我们既没有调用其他的构造函数,也没有显式调用超类的构造函数,那么编译器会为我们自动生成一个对超类构造函数的调用,比如一个无参构造函数:

public  Test01() {

    }

编译之后构造方法的字节码为:

public <init>()V
   L0
    LINENUMBER 4 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    LOCALVARIABLE this Lobjecttest/TestSuper; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1

上面代码INVOKESPECIAL java/lang/Object. ()V就是调用Object类的默认构造函数的指令。也就是说,如果我们显式调用超类的构造函数,那么该调用必须放在构造函数所有代码的最前面,也就是必须是构造函数的第一条指令。正因为如此,Java才可以使得一个对象在初始化之前其所有的超类都被初始化完成,并保证创建一个完整的对象出来。

如果在构造方法1中调用构造方法2,构造方法1必须在第一行调用,否则不能通过编译,而对超类的构造方法的调用实在内层,也就是super方法是在构造方法2中调用。如下:

public  Test02() {

}
public  Test02(int n) {
    this();
    this.n = n;

}

编译后的字节码为:

public <init>()V
   L0
    LINENUMBER 6 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
   L1
    LINENUMBER 8 L1
    RETURN
   L2
    LOCALVARIABLE this Lobjecttest/Test02; L0 L2 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public <init>(I)V
   L0
    LINENUMBER 11 L0
    ALOAD 0
    INVOKESPECIAL objecttest/Test02.<init> ()V
   L1
    LINENUMBER 12 L1
    ALOAD 0
    ILOAD 1
    PUTFIELD objecttest/Test02.n : I
   L2
    LINENUMBER 14 L2
    RETURN
   L3
    LOCALVARIABLE this Lobjecttest/Test02; L0 L3 0
    LOCALVARIABLE n I L0 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2

 小结

在准备实例化一个类的对象前,首先准备实例化该类的父类,如果该类的父类还有父类,那么准备实例化该类的父类的父类,依次递归直到递归到Object类。此时,首先实例化Object类,再依次对以下各类进行实例化,直到完成对目标类的实例化。具体而言,在实例化每个类时,都遵循如下顺序:先依次执行实例变量初始化和实例代码块初始化,再执行构造函数初始化。也就是说,编译器会将实例变量初始化和实例代码块初始化相关代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前

几个对象初始化的问题

    一个实例变量在对象初始化的过程中会被赋值几次

    JVM在为一个对象分配完内存之后,会给每一个实例变量赋予默认值,这个时候实例变量被第一次赋值,这个赋值过程是没有办法避免的。如果我们在声明实例变量x的同时对其进行了赋值操作,那么这个时候,这个实例变量就被第二次赋值了。如果我们在实例代码块中,又对变量x做了初始化操作,那么这个时候,这个实例变量就被第三次赋值了。如果我们在构造函数中,也对变量x做了初始化操作,那么这个时候,变量x就被第四次赋值。也就是说,在Java的对象初始化过程中,一个实例变量最多可以被初始化4次

    下面代码的执行结果是怎样

public class StaticTest {

    int a = 1;    // 实例变量

    static StaticTest STATIC = new StaticTest();

    static {   //静态代码块
        System.out.println("static block");
    }
    static int B = 2;     // 静态变量

    {       // 实例代码块
        System.out.println("code block");
    }

    StaticTest() {    // 实例构造器
        System.out.println("constract run");
        System.out.println("a=" + a + ",B=" + b);
    }

    public static void staticFunction() {   // 静态方法
        System.out.println("static function");
    }

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

输出结果:

code block
constract run
a=1,b=2
static block
static function

首先明确一个问题就是

  • 实例初始化不一定要在类初始化结束之后才开始初始化
  • 类的生命周期是:加载->验证->准备->解析->初始化->使用->卸载,并且只有在准备阶段和初始化阶段才会涉及类变量的初始化和赋值

下面逐步分析:

  1. 因为在main方法调用了静态变量开始类加载,在类的准备阶段需要做的是为类变量(static变量)分配内存并设置默认值(零值),因此在该阶段结束后,类变量STATIC将变为null、B变为0。
  2. 在类的初始化阶段需要做的是执行类构造器(),需要指出的是,类构造器本质上是编译器收集所有静态语句块和类变量的赋值语句按语句在源码中的顺序合并生成类构造器()。因此,对上述程序而言,JVM将先执行第一条静态变量的赋值语句,然后再给静态变量b赋值。
  3. 最核心的点来了:在同一个类加载器下,一个类型只会被初始化一次。所以,一旦开始初始化一个类型,无论是否完成,后续都不会再重新触发该类型的初始化阶段了(只考虑在同一个类加载器下的情形)。因此,在实例化上述程序中的STATIC变量时(
        static StaticTest STATIC = new StaticTest();),实际上是把实例初始化嵌入到了静态变量初始化流程中,并且在上面的程序中,嵌入到了静态初始化的起始位置。这就导致了实例初始化完全发生在静态代码块初始化之前,当然,这也是导致a为1,B为0的原因。
  4. 接下来开始对象初始化,父类构造函数>实例变量的赋值>代码块的执行>构造函数
  5. 至此static StaticTest STATIC静态赋值完成,接下里继续类的初始化,按照源码的顺序执行静态代码块,静态变量B的赋值,直到类的初始化完成。

 

顺序总结:

类加载准备阶段(类变量的设置默认值)--> 静态语句块与类变量赋值语句 --> 父类构造函数 --> 实例变量赋值与实例代码块 --> 构造函数。

 

 

来源于:https://blog.csdn.net/TheLudlows/article/details/82561596