Java 对象的建立过程

类的初始化与实例化
一个 Java 对象的建立过程每每包括类的初始化 和 实例化 两个阶段。
Java 规范规定一个对象在能够被使用以前必需要被正确地初始化。在类初始化过程当中或初始化完毕后,根据具体状况才会去对类进行实例化。在实例化一个对象时,JVM 首先会检查相关类型是否已经加载并初始化,若是没有,则 JVM 当即进行加载并调用类构造器完成类的初始化。
Java 对象的建立方式
一个对象在能够被使用以前必需要被正确地实例化。在 Java 程序中,有多种方法能够建立对象,最直接的一种就是使用 new 关键字来调用一个类的构造函数显式地建立对象。这种方式是由执行类的实例建立表达式建立对象。除此以外,还可使用反射机制 (Class 类的 newInstance 方法、Constructor 类的newInstance 方法)、使用 Clone 方法、使用反序列化等方式建立对象。
使用 new 关键字建立对象
这是最多见、最简单的建立对象的方式,经过这种方式能够调用任意的构造函数(无参的和有参的)建立对象。
使用 Class 类的 newInstance 方法 (反射机制) 。事实上 Class 类的 newInstance 方法内部调用的是 Constructor 类的 newInstance 方法,至关因而调用无参的构造器建立对象。
使用 Constructor 类的 newInstance 方法 (反射机制) 。该方法和 Class 类中的 newInstance 方法相似,不一样的是 Constructor 类的 newInstance 方法能够调用有参数的和私有的构造函数。
使用Clone方法建立对象
调用一个对象的 clone 方法,JVM 都会建立一个新的、同样的对象。特别须要说明的是,用 clone 方法建立对象的过程当中并不会调用任何构造函数。如何使用 clone 方法以及浅克隆/深克隆机制。简单而言,要想使用 clone 方法,就必须先实现 Cloneable 接口并实现其定义的 clone 方法,这也是原型模式的应用。
使用 (反) 序列化机制建立对象
当反序列化一个对象时,JVM会建立一个单独的对象,在此过程当中,JVM并不会调用任何构造函数。为了反序列化一个对象,对应的类须要实现 Serializable 接口。
从 Java 虚拟机层面看,除了使用 new 关键字建立对象的方式外,其余方式所有都是经过转变为 invokevirtual 指令直接建立对象的。
Java 对象的建立过程
当一个对象被建立时,虚拟机就会为其分配内存来存放对象本身的实例变量及其继承父类的实例变量 (即便继承超类的实例变量有可能被隐藏也会被分配空间) 。在为这些实例变量分配内存的同时,这些实例变量也会被赋予默认值。在内存分配完成以后,Java 虚拟机就会开始对新建立的对象进行初始化。在 Java 对象初始化过程当中,主要涉及三种执行对象初始化的结构,分别是实例变量初始化、实例代码块初始化以及构造函数初始化。
实例变量初始化与实例代码块初始化
在定义(声明)实例变量的同时,能够直接对实例变量进行赋值或者使用实例代码块对其进行赋值。若是以这两种方式为实例变量进行初始化,那么它们将在构造函数执行以前完成这些初始化操做。实际上,若是对实例变量直接赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,而且这些代码会被放在对超类构造函数的调用语句以后 (构造函数的第一条语句必须是超类构造函数的调用语句) ,构造函数自己的代码以前。
特别须要注意的是,Java 是按照前后顺序来执行实例变量初始化和实例初始化器中的代码,而且不容许顺序靠前的实例代码块初始化在其后面定义的实例变量。这么作是为了保证一个变量在被使用以前已经被正确地初始化。
构造函数初始化
实例变量初始化与实例代码块初始化老是发生在构造函数初始化以前。Java 中的每个类中都至少会有一个构造函数,若是没有显式定义构造函数,那么 JVM 会为它提供一个默认无参的构造函数。在编译生成的字节码中,这些构造函数会被命名成 () 方法 (参数列表与 Java 语言中构造函数的参数列表相同) 。Java 要求在实例化类以前,必须先实例化其超类,以保证所建立实例的完整性。
事实上,这一点是在构造函数中保证的:Java 强制要求除 Object 类 (Object 是 Java 的顶层类,没有超类) 以外全部类的构造函数中的第一条语句必须是超类构造函数的调用语句或者是类中定义的其余的构造函数。若是既没有调用其余的构造函数,也没有显式调用超类的构造函数,那么编译器会自动生成一个对超类构造函数的调用。
若是显式调用超类的构造函数,那么该调用必须放在构造函数全部代码的最前面。正由于如此,Java 才可使得一个对象在初始化以前其全部的超类都被初始化完成,并保证建立一个完整的对象出来。特别地,若是在一个构造函数中调用另一个构造函数则不能显式调用超类的构造函数,并且要另外一个构造函数放在构造函数全部代码的最前面。
Java 经过对构造函数做出上述限制保证一个类的实例可以在被使用以前正确地初始化。
1.Java普通对象的建立
这里讨论的仅仅是普通Java对象,不包含数组和Class对象。
1.1new指令
虚拟机遇到一条new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,而且检查这个符号引用表明的类是否已被加载、解析和初始化过。若是没有,那么须先执行相应的类加载过程。
1.2分配内存
接下来虚拟机将为新生代对象分配内存。对象所需的内存的大小在类加载完成后即可彻底肯定。分配方式有“指针碰撞(Bump the Pointer)”和“空闲列表(Free List)”两种方式,具体由所采用的垃圾收集器是否带有压缩整理功能决定。
1.3初始化
内存分配完成后,虚拟机须要将分配到的内存空间都初始化为零值(不包括对象头),这一步操做保证了对象的实例字段在Java代码中能够不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
1.4对象的初始设置
接下来虚拟机要对对象进行必要的设置,例如这个对象是哪一个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息存放在对象的对象头(Object Header)之中。根据虚拟机当前的运行状态的不一样,如对否启用偏向锁等,对象头会有不一样的设置方式。
1.5<init>方法
在上面的工做都完成了以后,从虚拟机的角度看,一个新的对象已经产生了,可是从Java程序的角度看,对象建立才刚刚开始—<init>方法尚未执行,全部的字段都还为零。因此,通常来讲,执行new指令后悔接着执行init方法,把对象按照程序员的意愿进行初始化(应该是将构造函数中的参数赋值给对象的字段),这样一个真正可用的对象才算彻底产生出来。
2.Java对象内存布局
在HotSpot虚拟机中,对象在内存中存储的布局能够分为3块区域:对象头(Header)、实例数据(Instance Data)、对其填充(Padding)。
2.1对象头
HotSpot虚拟机的对象头包含两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等。
对象的另外一部分类型指针,即对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例(并非全部的虚拟机实现都必须在对象数据上保留类型指针,也就是说,查找对象的元数据信息并不必定要通过对象自己)。
若是对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据。
元数据:描述数据的数据。对数据及信息资源的描述信息。在Java中,元数据大多表示为注解。
2.2实例数据
实例数据部分是对象真正存储的有效信息,也是在程序代码中定义的各类类型的字段内容,不管从父类继承下来的,仍是在子类中定义的,都须要记录起来。这部分的存储顺序会虚拟机默认的分配策略参数和字段在Java源码中定义的顺序影响(相同宽度的字段老是被分配到一块儿)。
2.3对齐填充
对齐填充部分并非必然存在的,也没有特别的含义,它仅仅起着占位符的做用。因为HotSpot VM的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是说,对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),所以,当对象实例数据部分没有对齐时,就须要经过对齐填充来补全。
你们都知道,java使用new 关键字进行对象的建立,但这只是从语言层次上理解了对象的建立,下边咱们从jvm的角度来看看,对象是怎么被建立出来的,即对象的建立过程。
对象的建立大概分为如下几步:
1:检查类是否已经被加载;
2:为对象分配内存空间;
3:为对象字段设置零值;
4:设置对象头;
5:执行构造方法。
第一步,当程序遇到new 关键字时,首先会去运行时常量池中查找该引用所指向的类有没有被虚拟机加载,若是没有被加载,那么会进行类的加载过程,若是已经被加载,那么进行下一步,为对象分配内存空间;
第二步,加载完类以后,须要在堆内存中为该对象分配必定的空间,该空间的大小在类加载完成时就已经肯定下来了,这里多说一点,为对象分配内存空间有两种方式:
(1)第一种是jvm将堆区抽象为两块区域,一块是已经被其余对象占用的区域,另外一块是空白区域,中间经过一个指针进行标注,这时只须要将指针向空白区域移动相应大小空间,就完成了内存的分配,固然这种划分的方式要求虚拟机的对内存是地址连续的,且虚拟机带有内存压缩机制,能够在内存分配完成时压缩内存,造成连续地址空间,这种分配内存方式成为“指针碰撞”,可是很明显,这种方式也存在一个比较严重的问题,那就是多线程建立对象时,会致使指针划分不一致的问题,例如A线程刚刚将指针移动到新位置,可是B线程以前读取到的是指针以前的位置,这样划份内存时就出现不一致的问题,解决这种问题,虚拟机采用了循环CAS操做来保证内存的正确划分;
(2)第二种也是为了解决第一种分配方式的不足而建立的方式,多线程分配内存时,虚拟机为每一个线程分配了不一样的空间,这样每一个线程在分配内存时只是在本身的空间中操做,从而避免了上述问题,不须要同步。固然,当线程本身的空间用完了才须要需申请空间,这时候须要进行同步锁定。为每一个线程分配的空间称为“本地线程分配缓冲(TLAB)”,是否启用TLAB须要经过 -XX:+/-UseTLAB参数来设定。
第三步,分配完内存后,须要对对象的字段进行零值初始化,对象头除外,零值初始化意思就是对对象的字段赋0值,或者null值,这也就解释了为何这些字段在不须要进程初始化时候就能直接使用;
第四步,这里,虚拟机须要对这个将要建立出来的对象,进行信息标记,包括是否为新生代/老年代,对象的哈希码,元数据信息,这些标记存放在对象头信息中,对象头很是复杂,这里不做解释,能够另行百度;
第五步,也就是最后一步,执行对象的构造方法,这里作的操做才是程序员真正想作的操做,例如初始化其余对象啊等等操做,至此,对象建立成功。
java中个,建立一个对象须要通过五步,分别是类加载检查、分配内存、初始化零值、设置对象头和执行初始化init()。html

  1. 类加载检查
    在java中,new一个对象的时候,java虚拟机会首先去检查这个指令的参数是否能在常量池中找到这个对象对应的类的符号引用,检查这个符号引用表明的类是否被类加载器加载、解析和初始化;若是没有,则必需要进行类加载。
  2. 分配内存
    在类加载以后,虚拟机会为将要的ThinkMarkets代理申请www.kaifx.cn/broker/thinkmarkets.html建立的对象分配内存,对象所需内存的大小在类加载完成即可彻底肯定,给对象分配内存是要在java堆中划分出一块肯定的内存。在java堆内存分配通常有两种方式,指针碰撞和空闲列表。
    (1)指针碰撞
    在java堆规整的状况下,适合采用指针碰撞方式。用过的内存所有整合到以便,没有用过的内存放在另一边,中间有一个分界值指针,用来将用过的内存与空闲内存分隔开来,当给新生对象分配内存时,指针便会向空闲内存区域移动。
    (2)空闲列表
    在java堆不规整的状况下,适合采用空闲列表方式。这种方式中,java虚拟机会维护一个列表,该列表是记录内存的块是不是可用的,当为新生对象分配内存的时候,会找一块足够大的内存分配给新生对象,以后更新这个列表。
    java堆是否规整由java虚拟机采用的垃圾收集器是否有压缩整理的功能决定。
  3. 初始化零值
    在给新生对象分配完内存完以后,虚拟机须要将分配到的内存空间都初始化为零值,这步操做保证了对象的实例字段在java代码中能够不赋初值就能够直接使用。
  4. 设置对象头
    初始化零值以后,要对新生对象设置对象头。对象头中包含类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。根据虚拟机当前运行状态的不一样,对象头也会有不一样的设置方式。
  5. 初始化(执行init方法)在给对象设置完对象头以后,虚拟机已经将一个对象产生了,此时,方法没有执行,对象的全部字段都为零值,零值的对象在程序中没有使用意义,只有初始化以后,对象才能真正体现出做用。此外,虚拟机建立java对象的时候,要保障线程的安全,虚拟机采用两种方式来保证线程安全。Java建立对象的过程简单记录一下Java建立对象的过程,就是new一个对象的时候发生了哪些事情。Java程序执行的过程在此不做说明,对象的建立过程只是程序执行过程的一部分。有关整个程序执行的过程,等熟悉了虚拟机以后在做说明。对象建立过程简述Java中对象的建立就是在堆上分配内存空间的过程,此处说的对象建立仅限于new关键字建立的普通Java对象,不包括数组对象的建立。大体过程以下:检测类是否被加载为对象分配内存为分配的内存空间初始化零值对对象进行其余设置执行init方法检测类是否被加载当虚拟机执行到new时,会先去常量池中查找这个类的符号引用。若是能找到符号引用,说明此类已经被加载到方法区(方法区存储虚拟机已经加载的类的信息),能够继续执行;若是找不到符号引用,就会使用类加载器执行类的加载过程,类加载完成后继续执行。为对象分配内存类加载完成之后,虚拟机就开始为对象分配内存,此时所需内存的大小就已经肯定了。只须要在堆上分配所须要的内存便可。具体的分配内存有两种状况:第一种状况是内存空间绝对规整,第二种状况是内存空间是不连续的。对于内存绝对规整的状况相对简单一些,虚拟机只须要在被占用的内存和可用空间之间移动指针便可,这种方式被称为指针碰撞。对于内存不规整的状况稍微复杂一点,这时候虚拟机须要维护一个列表,来记录哪些内存是可用的。分配内存的时候须要找到一个可用的内存空间,而后在列表上记录下已被分配,这种方式成为空闲列表。分配内存的时候也须要考虑线程安全问题,有两种解决方案:第一种是采用同步的办法,使用CAS来保证操做的原子性。另外一种是每一个线程分配内存都在本身的空间内进行,便是每一个线程都在堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB),分配内存的时候再TLAB上分配,互不干扰。为分配的内存空间初始化零值对象的内存分配完成后,还须要将对象的内存空间都初始化为零值,这样能保证对象即便没有赋初值,也能够直接使用。对对象进行其余设置分配完内存空间,初始化零值以后,虚拟机还须要对对象进行其余必要的设置,设置的地方都在对象头中,包括这个对象所属的类,类的元数据信息,对象的hashcode,GC分代年龄等信息。执行init方法执行完上面的步骤以后,在虚拟机里这个对象就算建立成功了,可是对于Java程序来讲还须要执行init方法才算真正的建立完成,由于这个时候对象只是被初始化零值了,尚未真正的去根据程序中的代码分配初始值,调用了init方法以后,这个对象才真正能使用。到此为止一个对象就产生了,这就是new关键字建立对象的过程。过程以下:检测类是否被加载–>为对象分配内存空间–>初始零值–>进行必要的设置–>调用init方法进行初始化。对象的建立过程:类加载检查-->分配内存-->初始化零值-->设置对象头-->执行init方法一、类加载检查:虚拟机遇到一条new指令时,先检查这个指令的参数可否在常量池中定位到一个类的符号引用,并检查这个符号引用表明的类是否已被ji加载、解析和初始化过。若是没有,则先进行类的加载过程。二、分配内存:有两种方式指针碰撞:假设Java堆中的内存是规整的,用过的内存在一边,空闲的在另外一边,中间有一个指针做为分界点的指示器,所分配的内存就把那个指针向空闲那边挪动一段与对象大小相等的距离。空闲列表:若是Java堆中的内存不是规整的,虚拟机必须维护一个列表,记录哪些内存块可用的,分配时从列表中找到一块足够大的空间划分给对象,并更新列表的记录。在划分可用空间时,会遇到线程安全的问题。解决这个问题有两种方案。第一种:对分配内存空间的动做进行同步处理--虚拟机采用CAS配上失败重试的方式保证更新操做的原子性。另外一种是把内存分配的动做安装现场划分在不一样的空间之中进行,即每一个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(TLAB)。那个线程须要分配内存,就在哪一个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才须要同步锁定。是否使用TLAB,-XX:+UseTLAB参数来设定。三、初始化零值 将分配到的内存空间都初始化为零值,若是用TLAB,则在TLAB分配时初始化为零值。四、设置对象头:主要设置类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。五、执行init方法初始化。类加载过程当JVM第一次要使用一个类的时候,须要加载这个类;首先根据classpath的配到硬盘上找这个类的class文件(若是没有配置classpath,就到当前位置找);若是找到这个class,就加载到方法区; a) 分别将这个类的静态成员加载到静态区域,非静态成员加载到非静态区域; b) 在静态区域为全部静态成员变量分配空间,赋默认值; c) 为全部静态成员变量显示赋值 d) 执行全部静态代码块 (c和d具体顺序是按照代码书写的前后顺序) 等到静态代码块都执行完毕,类加载完成;对象的建立过程JVM遇到new关键字,首先回去堆内存中开辟空间;为全部非静态的成员变量分配空间,赋默认值;调用相应的构造函数进栈执行;构造函数执行时首先要执行隐式三步: a) super():调用父类构造函数 // 若是第一行是this就调用this b) 给对象中全部非静态成员变量显示赋值; c) 执行构造代码块; (b和c具体执行顺序,是按照代码书写的前后顺序)隐式三步执行结束后,开始执行构造函数中的代码; 等到构造函数结束出栈,对象才算建立完成
相关文章
相关标签/搜索