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