类的初始化与实例化
一个 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()。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。