精华内容
下载资源
问答
  • 深入理解Java对象的创建过程:类的初始化与实例化

    万次阅读 多人点赞 2017-05-18 14:17:45
    在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。...本文试图对JVM执行类初始化和实例化的过程做一个详细深入地介绍,以便从Java虚拟机的角度清晰解剖一个Java对象的创建过程

    摘要:

      在Java中,一个对象在可以被使用之前必须要被正确地初始化,这一点是Java规范规定的。在实例化一个对象时,JVM首先会检查相关类型是否已经加载并初始化,如果没有,则JVM立即进行加载并调用类构造器完成类的初始化。在类初始化过程中或初始化完毕后,根据具体情况才会去对类进行实例化。本文试图对JVM执行类初始化和实例化的过程做一个详细深入地介绍,以便从Java虚拟机的角度清晰解剖一个Java对象的创建过程。


    版权声明:

    本文原创作者:书呆子Rico
    作者博客地址:http://blog.csdn.net/justloveyou_/


    友情提示:

      一个Java对象的创建过程往往包括 类初始化 类实例化 两个阶段。本文的姊妹篇《 JVM类加载机制概述:加载时机与加载过程》主要介绍了类的初始化时机和初始化过程,本文在此基础上,进一步阐述了一个Java对象创建的真实过程。


    一、Java对象创建时机

      我们知道,一个对象在可以被使用之前必须要被正确地实例化。在Java代码中,有很多行为可以引起对象的创建,最为直观的一种就是使用new关键字来调用一个类的构造函数显式地创建对象,这种方式在Java规范中被称为 : 由执行类实例创建表达式而引起的对象创建。除此之外,我们还可以使用反射机制(Class类的newInstance方法、使用Constructor类的newInstance方法)、使用Clone方法、使用反序列化等方式创建对象。下面笔者分别对此进行一一介绍:


    1). 使用new关键字创建对象

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

      Student student = new Student();

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

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

      Student student2 = (Student)Class.forName("Student类全限定名").newInstance(); 
    或者:
      Student stu = Student.class.newInstance();

    3). 使用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 static void main(String[] args) throws Exception {
    
            Constructor<Student> constructor = Student.class
                    .getConstructor(Integer.class);
            Student stu3 = constructor.newInstance(123);
        }
    }

      使用newInstance方法的这两种方式创建对象使用的就是Java的反射机制,事实上Class的newInstance方法内部调用的也是Constructor的newInstance方法。


    4). 使用Clone方法创建对象

      无论何时我们调用一个对象的clone方法,JVM都会帮我们创建一个新的、一样的对象,特别需要说明的是,用clone方法创建对象的过程中并不会调用任何构造函数。关于如何使用clone方法以及浅克隆/深克隆机制,笔者已经在博文《 Java String 综述(下篇)》做了详细的说明。简单而言,要想使用clone方法,我们就必须先实现Cloneable接口并实现其定义的clone方法,这也是原型模式的应用。比如:

    public class Student implements Cloneable{
    
        private int id;
    
        public Student(Integer id) {
            this.id = id;
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            // TODO Auto-generated method stub
            return super.clone();
        }
    
        public static void main(String[] args) throws Exception {
    
            Constructor<Student> constructor = Student.class
                    .getConstructor(Integer.class);
            Student stu3 = constructor.newInstance(123);
            Student stu4 = (Student) stu3.clone();
        }
    }

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

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

    public class Student implements Cloneable, Serializable {
    
        private int id;
    
        public Student(Integer id) {
            this.id = id;
        }
    
        @Override
        public String toString() {
            return "Student [id=" + id + "]";
        }
    
        public static void main(String[] args) throws Exception {
    
            Constructor<Student> constructor = Student.class
                    .getConstructor(Integer.class);
            Student stu3 = constructor.newInstance(123);
    
            // 写对象
            ObjectOutputStream output = new ObjectOutputStream(
                    new FileOutputStream("student.bin"));
            output.writeObject(stu3);
            output.close();
    
            // 读对象
            ObjectInputStream input = new ObjectInputStream(new FileInputStream(
                    "student.bin"));
            Student stu5 = (Student) input.readObject();
            System.out.println(stu5);
        }
    }

    6). 完整实例

    public class Student implements Cloneable, Serializable {
    
        private int id;
    
        public Student() {
    
        }
    
        public Student(Integer id) {
            this.id = id;
        }
    
        @Override
        protected Object clone() throws CloneNotSupportedException {
            // TODO Auto-generated method stub
            return super.clone();
        }
    
        @Override
        public String toString() {
            return "Student [id=" + id + "]";
        }
    
        public static void main(String[] args) throws Exception {
    
            System.out.println("使用new关键字创建对象:");
            Student stu1 = new Student(123);
            System.out.println(stu1);
            System.out.println("\n---------------------------\n");
    
    
            System.out.println("使用Class类的newInstance方法创建对象:");
            Student stu2 = Student.class.newInstance();    //对应类必须具有无参构造方法,且只有这一种创建方式
            System.out.println(stu2);
            System.out.println("\n---------------------------\n");
    
            System.out.println("使用Constructor类的newInstance方法创建对象:");
            Constructor<Student> constructor = Student.class
                    .getConstructor(Integer.class);   // 调用有参构造方法
            Student stu3 = constructor.newInstance(123);   
            System.out.println(stu3);
            System.out.println("\n---------------------------\n");
    
            System.out.println("使用Clone方法创建对象:");
            Student stu4 = (Student) stu3.clone();
            System.out.println(stu4);
            System.out.println("\n---------------------------\n");
    
            System.out.println("使用(反)序列化机制创建对象:");
            // 写对象
            ObjectOutputStream output = new ObjectOutputStream(
                    new FileOutputStream("student.bin"));
            output.writeObject(stu4);
            output.close();
    
            // 读取对象
            ObjectInputStream input = new ObjectInputStream(new FileInputStream(
                    "student.bin"));
            Student stu5 = (Student) input.readObject();
            System.out.println(stu5);
    
        }
    }/* Output: 
            使用new关键字创建对象:
            Student [id=123]
    
            ---------------------------
    
            使用Class类的newInstance方法创建对象:
            Student [id=0]
    
            ---------------------------
    
            使用Constructor类的newInstance方法创建对象:
            Student [id=123]
    
            ---------------------------
    
            使用Clone方法创建对象:
            Student [id=123]
    
            ---------------------------
    
            使用(反)序列化机制创建对象:
            Student [id=123]
    *///:~

      从Java虚拟机层面看,除了使用new关键字创建对象的方式外,其他方式全部都是通过转变为invokevirtual指令直接创建对象的。


    二. Java 对象的创建过程

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


    1、实例变量初始化与实例代码块初始化

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

    public class InstanceVariableInitializer {  
    
        private int i = 1;  
        private int j = i + 1;  
    
        public InstanceVariableInitializer(int var){
            System.out.println(i);
            System.out.println(j);
            this.i = var;
            System.out.println(i);
            System.out.println(j);
        }
    
        {               // 实例代码块
            j += 3; 
    
        }
    
        public static void main(String[] args) {
            new InstanceVariableInitializer(8);
        }
    }/* Output: 
                1
                5
                8
                5
     *///:~

      上面的例子正好印证了上面的结论。特别需要注意的是,Java是按照编程顺序来执行实例变量初始化器和实例初始化器中的代码的,并且不允许顺序靠前的实例代码块初始化在其后面定义的实例变量,比如:

    public class InstanceInitializer {  
        {  
            j = i;  
        }  
    
        private int i = 1;  
        private int j;  
    }  
    
    public class InstanceInitializer {  
        private int j = i;  
        private int i = 1;  
    }  

      上面的这些代码都是无法通过编译的,编译器会抱怨说我们使用了一个未经定义的变量。之所以要这么做是为了保证一个变量在被使用之前已经被正确地初始化。但是我们仍然有办法绕过这种检查,比如:

    public class InstanceInitializer {  
        private int j = getI();  
        private int i = 1;  
    
        public InstanceInitializer() {  
            i = 2;  
        }  
    
        private int getI() {  
            return i;  
        }  
    
        public static void main(String[] args) {  
            InstanceInitializer ii = new InstanceInitializer();  
            System.out.println(ii.j);  
        }  
    }  

      如果我们执行上面这段代码,那么会发现打印的结果是0。因此我们可以确信,变量j被赋予了i的默认值0,这一动作发生在实例变量i初始化之前和构造函数调用之前。


    2、构造函数初始化

      我们可以从上文知道,实例变量初始化与实例代码块初始化总是发生在构造函数初始化之前,那么我们下面着重看看构造函数初始化过程。众所周知,每一个Java中的对象都至少会有一个构造函数,如果我们没有显式定义构造函数,那么它将会有一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成<init>()方法,参数列表与Java语言书写的构造函数的参数列表相同。

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

    public class ConstructorExample {  
    
    } 

      对于上面代码中定义的类,我们观察编译之后的字节码,我们会发现编译器为我们生成一个构造函数,如下,

    aload_0  
    invokespecial   #8; //Method java/lang/Object."<init>":()V  
    return  

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


      特别地,如果我们在一个构造函数中调用另外一个构造函数,如下所示,

    public class ConstructorExample {  
        private int i;  
    
        ConstructorExample() {  
            this(1);  
            ....  
        }  
    
        ConstructorExample(int i) {  
            ....  
            this.i = i;  
            ....  
        }  
    }  

      对于这种情况,Java只允许在ConstructorExample(int i)内调用超类的构造函数,也就是说,下面两种情形的代码编译是无法通过的:

    public class ConstructorExample {  
        private int i;  
    
        ConstructorExample() {  
            super();  
            this(1);  // Error:Constructor call must be the first statement in a constructor
            ....  
        }  
    
        ConstructorExample(int i) {  
            ....  
            this.i = i;  
            ....  
        }  
    }  

    或者,

    public class ConstructorExample {  
        private int i;  
    
        ConstructorExample() {  
            this(1);  
            super();  //Error: Constructor call must be the first statement in a constructor
            ....  
        }  
    
        ConstructorExample(int i) {  
            this.i = i;  
        }  
    }   

      Java通过对构造函数作出这种限制以便保证一个类的实例能够在被使用之前正确地初始化。


    3、 小结

      总而言之,实例化一个类的对象的过程是一个典型的递归过程,如下图所示。进一步地说,在实例化一个类的对象时,具体过程是这样的:

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

                 这里写图片描述

                    
      Ps: 关于递归的思想与内涵的介绍,请参见我的博文《 算法设计方法:递归的内涵与经典应用》


    4、实例变量初始化、实例代码块初始化以及构造函数初始化综合实例

      笔者在《 JVM类加载机制概述:加载时机与加载过程》一文中详细阐述了类初始化时机和初始化过程,并在文章的最后留了一个悬念给各位,这里来揭开这个悬念。建议读者先看完《 JVM类加载机制概述:加载时机与加载过程》这篇再来看这个,印象会比较深刻,如若不然,也没什么关系~~
      

    //父类
    class Foo {
        int i = 1;
    
        Foo() {
            System.out.println(i);             -----------(1)
            int x = getValue();
            System.out.println(x);             -----------(2)
        }
    
        {
            i = 2;
        }
    
        protected int getValue() {
            return i;
        }
    }
    
    //子类
    class Bar extends Foo {
        int j = 1;
    
        Bar() {
            j = 2;
        }
    
        {
            j = 3;
        }
    
        @Override
        protected int getValue() {
            return j;
        }
    }
    
    public class ConstructorExample {
        public static void main(String... args) {
            Bar bar = new Bar();
            System.out.println(bar.getValue());             -----------(3)
        }
    }/* Output: 
                2
                0
                2
     *///:~

      根据上文所述的类实例化过程,我们可以将Foo类的构造函数和Bar类的构造函数等价地分别变为如下形式:

        //Foo类构造函数的等价变换:
        Foo() {
            i = 1;
            i = 2;
            System.out.println(i);
            int x = getValue();
            System.out.println(x);
        }
        //Bar类构造函数的等价变换
        Bar() {
            Foo();
            j = 1;
            j = 3;
            j = 2
        }

      这样程序就好看多了,我们一眼就可以观察出程序的输出结果。在通过使用Bar类的构造方法new一个Bar类的实例时,首先会调用Foo类构造函数,因此(1)处输出是2,这从Foo类构造函数的等价变换中可以直接看出。(2)处输出是0,为什么呢?因为在执行Foo的构造函数的过程中,由于Bar重载了Foo中的getValue方法,所以根据Java的多态特性可以知道,其调用的getValue方法是被Bar重载的那个getValue方法。但由于这时Bar的构造函数还没有被执行,因此此时j的值还是默认值0,因此(2)处输出是0。最后,在执行(3)处的代码时,由于bar对象已经创建完成,所以此时再访问j的值时,就得到了其初始化后的值2,这一点可以从Bar类构造函数的等价变换中直接看出。


    三. 类的初始化时机与过程

      关于类的初始化时机,笔者在博文《 JVM类加载机制概述:加载时机与加载过程》已经介绍的很清楚了,此处不再赘述。简单地说,在类加载过程中,准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,而初始化阶段是真正开始执行类中定义的java程序代码(字节码)并按程序猿的意图去初始化类变量的过程。更直接地说,初始化阶段就是执行类构造器<clinit>()方法的过程。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态代码块static{}中的语句合并产生的,其中编译器收集的顺序是由语句在源文件中出现的顺序所决定。

      类构造器<clinit>()与实例构造器<init>()不同,它不需要程序员进行显式调用,虚拟机会保证在子类类构造器<clinit>()执行之前,父类的类构造<clinit>()执行完毕。由于父类的构造器<clinit>()先执行,也就意味着父类中定义的静态代码块/静态变量的初始化要优先于子类的静态代码块/静态变量的初始化执行。特别地,类构造器<clinit>()对于类或者接口来说并不是必需的,如果一个类中没有静态代码块,也没有对类变量的赋值操作,那么编译器可以不为这个类生产类构造器<clinit>()。此外,在同一个类加载器下,一个类只会被初始化一次,但是一个类可以任意地实例化对象。也就是说,在一个类的生命周期中,类构造器<clinit>()最多会被虚拟机调用一次,而实例构造器<init>()则会被虚拟机调用多次,只要程序员还在创建对象。

      注意,这里所谓的实例构造器<init>()是指收集类中的所有实例变量的赋值动作、实例代码块和构造函数合并产生的,类似于上文对Foo类的构造函数和Bar类的构造函数做的等价变换。


    四. 总结

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

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


      2、类的初始化过程与类的实例化过程的异同?

      类的初始化是指类加载过程中的初始化阶段对类变量按照程序猿的意图进行赋值的过程;而类的实例化是指在类完全加载到内存中后创建对象的过程。


      3、假如一个类还未加载到内存中,那么在创建一个该类的实例时,具体过程是怎样的?

      我们知道,要想创建一个类的实例,必须先将该类加载到内存并进行初始化,也就是说,类初始化操作是在类实例化操作之前进行的,但并不意味着:只有类初始化操作结束后才能进行类实例化操作。例如,笔者在博文《 JVM类加载机制概述:加载时机与加载过程》中所提到的下面这个经典案例:

    public class StaticTest {
        public static void main(String[] args) {
            staticFunction();
        }
    
        static StaticTest st = new StaticTest();
    
        static {   //静态代码块
            System.out.println("1");
        }
    
        {       // 实例代码块
            System.out.println("2");
        }
    
        StaticTest() {    // 实例构造器
            System.out.println("3");
            System.out.println("a=" + a + ",b=" + b);
        }
    
        public static void staticFunction() {   // 静态方法
            System.out.println("4");
        }
    
        int a = 110;    // 实例变量
        static int b = 112;     // 静态变量
    }/* Output: 
            2
            3
            a=110,b=0
            1
            4
     *///:~

      大家能得到正确答案吗?笔者已经在博文《 JVM类加载机制概述:加载时机与加载过程》中解释过这个问题了,此不赘述。
      
      总的来说,类实例化的一般过程是:父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成员变量和实例代码块 -> 子类的构造函数。


    五. 更多

      更多关于类初始化时机和初始化过程的介绍,请参见我的博文《 JVM类加载机制概述:加载时机与加载过程》

      更多关于类加载器等方面的内容,包括JVM预定义的类加载器、双亲委派模型等知识点,请参见我的转载博文《深入理解Java类加载器(一):Java类加载原理解析》

      关于递归的思想与内涵的介绍,请参见我的博文《 算法设计方法:递归的内涵与经典应用》


    引用:

    Java对象初始化详解
    Java中创建对象的几种方式

    展开全文
  • java对象的创建过程

    千次阅读 2018-09-03 20:54:02
    大家都知道,java使用new 关键字进行对象的创建,但这只是从语言层次上理解了对象的创建,下边我们从jvm的角度来看看,对象是怎么被创建出来的,即对象的创建过程。 对象的创建大概分为以下几步: 1:检查类是否...

    大家都知道,java使用new 关键字进行对象的创建,但这只是从语言层次上理解了对象的创建,下边我们从jvm的角度来看看,对象是怎么被创建出来的,即对象的创建过程。

    对象的创建大概分为以下几步:

    1:检查类是否已经被加载;

    2:为对象分配内存空间;

    3:为对象字段设置零值;

    4:设置对象头;

    5:执行构造方法。

    第一步,当程序遇到new 关键字时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟机加载,如果没有被加载,那么会进行类的加载过程,如果已经被加载,那么进行下一步,为对象分配内存空间;

    第二步,加载完类之后,需要在堆内存中为该对象分配一定的空间,该空间的大小在类加载完成时就已经确定下来了,这里多说一点,为对象分配内存空间有两种方式:

    (1)第一种是jvm将堆区抽象为两块区域,一块是已经被其他对象占用的区域,另一块是空白区域,中间通过一个指针进行标注,这时只需要将指针向空白区域移动相应大小空间,就完成了内存的分配,当然这种划分的方式要求虚拟机的对内存是地址连续的,且虚拟机带有内存压缩机制,可以在内存分配完成时压缩内存,形成连续地址空间,这种分配内存方式成为“指针碰撞”,但是很明显,这种方式也存在一个比较严重的问题,那就是多线程创建对象时,会导致指针划分不一致的问题,例如A线程刚刚将指针移动到新位置,但是B线程之前读取到的是指针之前的位置,这样划分内存时就出现不一致的问题,解决这种问题,虚拟机采用了循环CAS操作来保证内存的正确划分;

    (2)第二种也是为了解决第一种分配方式的不足而创建的方式,多线程分配内存时,虚拟机为每个线程分配了不同的空间,这样每个线程在分配内存时只是在自己的空间中操作,从而避免了上述问题,不需要同步。当然,当线程自己的空间用完了才需要需申请空间,这时候需要进行同步锁定。为每个线程分配的空间称为“本地线程分配缓冲(TLAB)”,是否启用TLAB需要通过 -XX:+/-UseTLAB参数来设定。

    第三步,分配完内存后,需要对对象的字段进行零值初始化,对象头除外,零值初始化意思就是对对象的字段赋0值,或者null值,这也就解释了为什么这些字段在不需要进程初始化时候就能直接使用;

    第四步,这里,虚拟机需要对这个将要创建出来的对象,进行信息标记,包括是否为新生代/老年代,对象的哈希码,元数据信息,这些标记存放在对象头信息中,对象头非常复杂,这里不作解释,可以另行百度;

    第五步,也就是最后一步,执行对象的构造方法,这里做的操作才是程序员真正想做的操作,例如初始化其他对象啊等等操作,至此,对象创建成功。

    这里其实讲的比较粗浅,只是对象创建的大概过程,例如对象头、类的加载等等都是非常复杂的过程,我会在接下来的博客中一一解释。文中有不对的地方,诚恳各位大佬指点。

     

     

    展开全文
  • Java对象的创建过程详解

    千次阅读 2020-03-30 17:07:27
    本文详细介绍了Java中对象的创建过程,包括对象的创建方式、分配内存、空间初始化、构造方法的执行等。

      本文详细介绍了Java中对象的创建过程,包括对象的创建方式、分配内存、空间初始化、构造方法的执行等。

    1 创建对象的方法有哪些?

    1. 运用New 关键字创建实例,这是最常用的创建对象方法。
    2. 运用反射,调用Java.lang.Class类当中newInstance方法。只能调用公共的无参构造函数。
    3. 运用反射,调用java.lang.reflect.Constructor类中的newInstance方法提供无参或有参实例。除了无参构造器,还可以调用有参数的/私有的/受保护的构造函数。事实上Class的newInstance方法内部调用Constructor的newInstance方法。这也是众多框架Spring、Hibernate、Struts等使用后者的原因。
    4. 调用对象的clone方法。必须先实现java.lang.Cloneable接口。
    5. 使用序列化和反序列化。必须先实现Serializable接口。
    6. 使用unsafe.allocateInstance(class)创建对象。Gson中使用到,关于unsafe详见此博客:JUC中的Unsafe类详解与使用案例

    方法1、2、3本质都会调用构造函数,都是常规的Java创建对象的new机制;而方法4、5、6不会调用构造函数。本次主要讲解调用构造函数的方式,即使用了new关键字。

    2 创建过程概述

    2.1 检查类的加载

      虚拟机遇到一条new指令时 ,首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有 ,那必须先执行相应的类加载过程。

    2.2 分配内存

      在类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。

    2.2.1 内存分配方式

    1. 如果Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞(Bump the Pointer)”。
    2. 如果Java堆中的内存并不是规整的,已使用的内存和空闲的内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录那些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表(Free List)”。

    选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。

    2.2.2 内存分配时的安全问题

      对象创建在虚拟机中是非常频繁地行为,仅仅是修改一个指针所指向的位置,在并发情况下也并不是线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
    解决这个问题有两种方案:

    1. 一种是对分配内存空间的动作进行同步处理-实际上虚拟机采用CAS配上失败冲的方式保证更新操作的原子性。
    2. 另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。那个线程要分配内存就在那个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定。

    2.3 空间初始化

      内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头), 如果使用TLAB方式分配内存 ,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。下面是各种数据类型的初始值:

    类型 默认值
    byte (byte)0
    short (short)0
    int 0
    long 0L
    float 0.0f
    double 0.0d
    boolean false
    char '/uoooo'(null)
    reference(引用类型) null

    2.4 其他必要的设置

      接下来 ,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息,这些信息存放在对象的对象头(Object Header ) 之中。 根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。这部分会在对象内存布局方面有讲解。

    2.5 执行< init >方法(new关键字专属)

      在上面工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象创建才刚刚开始——< init >方法(实例初始化方法,类初始化方法是< clinit > )还没有执行,所有的字段都还为零值。 所以,一般来说(由字节码中是否跟随invokespecial指令所决定,不走构造器的初始化方式没有这条指令),执行new指令之后会接着 执行< init >方法(子类的< init >方法中会首先对父类< init >方法的调用),把对象按照程序员的意愿进行初始化。然后将内存地址赋给栈内存中的变量,这样一个真正可用的对象才算完全产生出来。
      特别注意,对象初始化和指向栈空间内存这两步,先后发生顺序是随机的。这对单例模式中的双重检查锁具有重要影响!可能直接导致单例模式的失效!(使用JDK1.5之后volatile关键字可以避免,但是之前的当本则无法保证!)

    3 补充:特殊方法

    3.1 < init >和< clinit >

      在Java虚拟机层面上,Java编程语言中的构造器是以一个名为< init >的特殊实例初始化方法的形式出现的。
      < init >这个方法名称是由编译器命名的, 因为它并非一个合法的Java方法名字, 不可能通过程序编码的方式实现。实例初始化方法只能在实例的初始化期间, 通过Java虚拟机的invokespecial指令来调用, 而且只能在尚未初始化的实例上调用该指令。构造器的访问权限,也会约束由该构造器所衍生出来的实例初始化方法。
    在这里插入图片描述
      一个类或者接口最多可以包含不超过一个类或接口的初始化方法, 类或者接口就是通过这个方法完成初始化的这个方法是一个不包含参数的、返回类型为void的方法,名为< clinit >。
      < clinit > 这个名字也是由编译器命名的, 因为它并非一个合法的Java方法名字, 不可能通过Java程序编码的方式直接实现。类或接口的初始化方法由Java虚拟机自身隐式调用, 没有任何虚拟机字节码指令可以调用这个方法, 它只会在类的初始化阶段中由虚拟机自身调用。
      < clinit>()方法对于类或接口来说并不是必须的,如果一个类/接口中没有静态语句块,也没有对类变量的赋值操作,或者该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化或者该类仅包含静态 final 变量的类变量初始化语句,并且类变量初始化语句是编译时常量表达式,那么编译器可以不为这个类生成< clinit>()方法。

    3.2 区别

      < init >是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 构造方法时才会执行< init >方法,而< clinit >是类构造器方法,也就是在jvm进行类加载—验证—解析—初始化中的初始化阶段jvm会调用< clinit >方法。
      < init >将语句块、变量初始化、调用父类的构造器等操作放到该方法中,顺序为:1.父类变量初始化块/父类语句块 2.父类构造函数 3.子类变量初始化块/子类语句块 4.子类构造函数。
      < clinit >将静态语句块、静态变量初始化等操作放到该方法中,顺序为:1.父类静态变量初始化/父类静态语句块 2.子类静态变量初始化/子类静态语句块。
      从上面可以知道变量和语句块的初始化是与代码书写的顺序一致的。因此要注意顺序,在(静态)代码块中可以使用(静态)变量,但是被使用的(静态)变量必须在(静态)代码块前面声明。
      更多< clinit >的使用和规则在类加载部分。

    3 参考

    《深入理解Java虚拟机》
    《Java虚拟机规范》

    展开全文
  • Java对象的创建过程

    千次阅读 2019-03-21 21:39:23
    对象的创建过程: 类加载检查-->分配内存-->初始化零值-->设置对象头-->执行init方法 1、类加载检查:虚拟机遇到一条new指令时,先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这...

    对象的创建过程:

    类加载检查-->分配内存-->初始化零值-->设置对象头-->执行init方法

    1、类加载检查:虚拟机遇到一条new指令时,先检查这个指令的参数能否在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已被ji加载、解析和初始化过。如果没有,则先进行类的加载过程。

    2、分配内存:有两种方式

    指针碰撞:假设Java堆中的内存是规整的,用过的内存在一边,空闲的在另一边,中间有一个指针作为分界点的指示器,所分配的内存就把那个指针向空闲那边挪动一段与对象大小相等的距离。

    空闲列表:如果Java堆中的内存不是规整的,虚拟机必须维护一个列表,记录哪些内存块可用的,分配时从列表中找到一块足够大的空间划分给对象,并更新列表的记录。

    在划分可用空间时,会遇到线程安全的问题。解决这个问题有两种方案。第一种:对分配内存空间的动作进行同步处理--虚拟机采用CAS配上失败重试的方式保证更新操作的原子性。另一种是把内存分配的动作安装现场划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。那个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。是否使用TLAB,-XX:+UseTLAB参数来设定。

    3、初始化零值 将分配到的内存空间都初始化为零值,如果用TLAB,则在TLAB分配时初始化为零值。

    4、设置对象头:主要设置类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。

    5、执行init方法初始化。

     

    深入理解Java虚拟机

    展开全文
  • JDK1.8——Java对象的创建过程

    千次阅读 2020-08-16 20:22:20
    对象的创建过程如图: 这里解释一下什么是符号引用: 符号引用: 符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、...
  • JVM——对象的创建过程

    千次阅读 2018-03-23 20:11:51
    对象的创建过程:我们在Java代码中new 一个对象时,我们很难看到对象创建的过程,尤其是在jvm内存中的过程,下面来介绍一下对象创建在jvm中的过程: 对象实例在jvm中是存在于java堆中的,每一个对象实例都会占用...
  • java new对象的创建过程

    万次阅读 2018-07-29 07:00:54
    关于对象的创建过程一般是从new指令(我说的是JVM的层面)开始的(具体请看图1),JVM首先对符号引用进行解析,如果找不到对应的符号引用,那么这个类还没有被加载,因此JVM便会进行类加载过程(具体加载过程可参见我的...
  • 下面本文将详细介绍java语言中对象的创建过程,以及函数的调用过程。 class Person{ public String name="zhao"; public int ege; public static String country = "CN"; Person(String name,int ege){ this....
  • 前面几篇博文分别介绍了JAVA的Class文件格式、JVM的类加载机制和JVM的内存模型,这里就索性把java对象的创建过程一并说完,这样java对象的整个创建过程就基本上说明白了(当然你要有基础才能真正看明白)。经常有人问...
  • JVM 类加载机制、对象的创建过程

    千次阅读 2020-06-03 11:48:51
    目录对象的创建过程对象的内存布局   对象的创建过程 1、JVM执行一条new指令时,会先在常量池中定位指定类的符号引用,判断该类的class对象是否已加载、解析和初始化,如果未,则先加载加载、解析、初始化,...
  • Spring源码阅读之bean对象的创建过程

    千次阅读 2020-06-10 23:52:58
    Spring源码阅读之bean对象的创建过程 ​ Spring是通过IOC容器来管理对象的,该容器不仅仅只是帮我们创建了对象那么简单,它负责了对象的整个生命周期-创建、装配、销毁。这种方式成为控制反转(组件对象控制权的...
  • C++(类对象的创建过程与释放过程)

    千次阅读 2019-08-16 17:00:25
    C++类对象的创建过程与释放过程 如下图所示 创建释放过程代码举例 #include <iostream> using namespace std; class Man { public: Man(void) { cout<<"我是man的构造函数,我被调用了"<<...
  • 深入理解Java虚拟机之对象的创建过程Java作为一门面向对象语言,在程序运行过程中会产生大量的对象,在语言层面上来看,创建一个对象仅仅需要一个new关键字即可,但是在虚拟机中创建一个普通的Java对象(不包括Class...
  • 本文详细介绍了对象的创建过程如类的加载、内存的分配和初始化,分配内存时的同步方式,对象的初始化操作。还介绍了对象的内存布局,如对象头、实例数据、对齐填充。最后介绍了 Hotspot 如何进行对象的访问定位。
  • JAVA虚拟机:对象的创建过程

    千次阅读 2016-07-15 10:46:45
    简要说明的话,Java对象的创建过程分为下面几步: 1、执行相关检查; 2、为对象分配内存,将分配到的内存空间都初始化为零值; 3、进行构造代码块和构造函数的初始化  下面详细介绍这几个步骤: 1、执行相关检查 ...
  • JAVA对象的创建过程

    千次阅读 2019-08-01 18:00:16
    在图中可以清楚看到jvm在创建对象时候执行流程,下面具体讨论一下每个步骤: 在程序进行加载时候,他类信息,常量,静态变量等数据就会存放在方法区(常量池)中。 在执行new操作时候,就会去方法区去...
  • 子类对象的创建过程

    千次阅读 2019-04-21 15:47:06
    简单说一说子类创建对象的时候,子类和其父类的创建过程。 如下所示: class A{ int v1=1; static int v2=2; static{} public A(){} } class B extends A{ int v3=3; static int v4=4; static{} public B(){} } 一:...
  • 对象的创建过程(new 的过程)

    千次阅读 2017-08-26 11:07:56
    在语言层面上,创建对象通常仅仅是使用一个new关键字而已,而在虚拟机中,对象(仅限于普通Java对象的创建又是怎样一个过程呢?虚拟机遇到一条new指令时,首先将去检查这个指令的参数能否在常量池中定位到一个类的...
  • JVM源码分析之Java对象的创建过程

    千次阅读 2017-03-22 16:36:30
    接着上篇《JVM源码分析之Java类加载过程》,本文将基于HotSpot实现对Java对象的创建过程进行深入分析。 定义两个简单的类AAA和BBB通过“javap -c AAA“`查看编译之后的字节码,具体如下:Java中的new
  • JVM - 对象的创建过程

    千次阅读 2018-11-14 21:22:00
    以new关键字创建对象的过程 执行程序Object obj = new Object(); 当 jvm 遇到new指令时: 1. 首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、验证...
  • JVM源码分析之Java对象的创建过程 接着上篇《JVM源码分析之Java类加载过程》,本文将基于HotSpot实现对Java对象的创建过程进行深入分析。 定义两个简单的类AAA和BBB 通过``javap -c AAA```查看...
  • 【JVM】Java对象的创建过程

    千次阅读 2021-05-08 16:36:23
    对象所需内存大小在类加载完成后即可确定,从Java堆中划分一块确定大小内存。 内存分配两种方式: 2.1 指针碰撞 适用场合:堆内存规整 原理:用过内存放一边,没用过放另一边,中间有一个分界值指针。向着...
  • jvm对象的创建过程

    千次阅读 2014-04-30 10:04:05
    2虚拟机为新生对象分配内存,分配方式有指针碰撞,空闲列表.(这里对象的创建是非常频繁的,需要考虑原子性问题)3内存分配后,jvm要将分配到的内存初始化为零值。4jvm对对象进行必要的设置,入对象是哪个类的,对象的...
  • String对象的创建过程中的内存分配

    千次阅读 2010-07-25 10:51:00
    String对象的创建过程中的内存分配1.JVM数据区内存模型 <br />先来了解一下JVM运行时数据区的内存模型。 《深入Java虚拟机》书中是这样描述的:JVM运行时数据区的内存模型由五部分组成: 方法区(类型...
  • java语言中的子类对象和父类对象的创建过程!class Fu { protected String name = "李四"; protected int age = 6; static { System.out.println("父类静态代码块,给类初始化用的。"); } {

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 49,025
精华内容 19,610
关键字:

对象的创建过程