类的初始化与实例化
一个 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