JVM- 技术专题 - 对象的实例化过程

用户头像
李博@Alex
关注
发布于: 2020 年 10 月 12 日
JVM-技术专题-对象的实例化过程

一、Java对象创建的方式(5种)

1、使用new关键字创建

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

我们也可以通过Java的反射机制使用Class类的newInstance方法来创建对象,事实上,这个newInstance方法调用无参的构造器创建对象,所以Student类中一定要有无参构造器,否则报错。

3、使用Constructor类的newInstance方法(反射机制)

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

4、使用Clone方法创建

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

具体代码如下所示:



总结:最简单的就是用new关键字创建对象,利用反射有两种方法,需要调用构造器,第二种方法一定调用的是无参构造器,clone和序列化并没有额外调用构造器。

二、对象的实例化过程



对象的创建过程

1、类加载

当虚拟机遇到一个new指令的时候,会先去检测这个指令的参数是否能定位到这个类的符号引用,并检查这个类是否被加载、解析、初始化过(在JVM的方法区中检查)。如果没有,则执行类加载(类加载机制

2、内存分配

在类加载通过之后,虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定,相当于从Java堆中抽取一块内存出来;而根据内存的是否绝对规整,分为指针碰撞空闲列表两种分配方式:

指针碰撞:假设Java堆中的内存是绝对规整的,分为空闲非空闲两种,中间用一个指针当做划分界限的指示器;当一个新对象需要分配对象时,相当于把指针向空闲区域移动一段与对象大小相等的距离。

空闲列表:假设Java堆的内存不是绝对规整的,空闲和非空闲是相互交错的,那就需要一个OopMap列表,用来记录哪些内存块是可以用的,在对象分配内存时,划分一块大小相等的区域给对象,并更新这个列表

从上面的解释看,用哪种分配方式,是通过Java堆的内存块是否绝对规整决定的。

堆内存是否规整,主要是看 GC 回收了内存之后是否包含压缩或者整理功能.如果有,那么内存就比较规整.否则如果没有,创建对象就需要采用空闲列表的方式.

比如:serial,ParNew 等带有整理的收集器,可以使用指针碰撞.

CMS 使用简单清除的算法,可以使用空闲列表.

但对象的创建是频繁的,在并发的情况,多线程不一定是安全的,即存在A对象在分配内存,指针还未来得及修改,B对象也同时使用了原来的指针来分配对象。所以又衍生了两种解决办法,CAS+失败重试 和 TLAB两种方式


CAS+失败重试:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性 (关于CAS锁,是乐观锁的一种实现,解释起来也比较麻烦,可以参考这里https://www.cnblogs.com/javalyy/p/8882172.html)



TLAB:本地线程分配缓冲,把内存分配的动作按照线程分配划分在不同的空间中进行,即每个线程在Java堆中预先分配一小块内存,哪个线程需要需要分配,先在 TLAB 中分配,用完了并重新分配新的TLAB时,才需要同步锁定。

3、初始值为零

在内存分配完成之后,虚拟机需要将分配到的内存空间初始化为零值 (除对象头外),这一步操作也保证了对象的实例字段在java代码中可以不赋初始值就可以使用,因为程序能访问这些字段的数据类型所对应的零值

4、设置对象头

初始值设置之后,怎么知道对象是哪个类的实例,如何才能找到类的元数据信息、哈希码、GC分代年龄等信息呢?这就需要对对象头进行一些必要的设置,才能定位到。

5、入栈、执行init指令

从虚拟机来看,对象已经分配产生完成了,且入栈了;但 Java 程序来看,这才刚开始,所以,new 之后,则执行 init 方法,进行初始化。

6、Java对象的内存分布(即实例化后的对象在堆中的分布)

对象在内存中的存储布局可分为3部分:

对象头

其中对象头又可以细分为两部分:

1、存储对象自身运行时数据:如哈希码、GC分代年龄、锁状态标志、线程持有的、偏向线程ID等信息

2、类型指针:即对象指向它的类元数据的指针,虚拟机通过这个来确定这个对象是哪个类的实例(比如是指向栈中的类声明)

实例数据

是对象真正存储的有效信息,比如程序中定义的各种类型的字段内容,无论父类和子类都会记录下来;在分配时,相同宽度的字段会被分配到一起,这也是父类定义的变量会出现在子类之前的原因。

对齐填充

没啥实际意义,就是为了保证对象是8个字节的整数倍,没对齐时,用来补全而已。

7、对象的访问定位

使用对象时,通过栈上的reference数据来操作堆上的具体对象。

建立对象是为了使用对象,Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象但这些访问方式取决于虚拟机实现而定,目前主流有句柄和直接指针两种:

句柄:从Java 堆中划分出一块内存用来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄包含了对象实例数据与类型数据各自的具体地址信息,如下图(图片来自Java虚拟机第三版)

直接指针:在直接指针中,reference 储存的就是对象地址,所以,需要考虑的是如何防止访问类型数据的相关信息(图片来自Java虚拟机第三版)

优点介绍:

句柄:使用句柄好处是,reference中存放的是文档的句柄地址,对象被移动时,只改变句柄的实例数据指针,而reference 本身不需要修改

直接指针:使用直接指针的最大好处就是速度更快,节省了指针定位的开销;

HotSpot 使用第二种方式进行对象访问的.

三、对象的具体实例化过程

1, 在堆内存中开辟一块空间

2, 开辟空间分配一个地址(指针碰撞或者空闲列表两种分配方式)

3, 把对象的所有非静态成员加载到所开辟的空间下(从方法区的非静态区域中加载,类加载的时候.class文件的非静态内容就是加载到这里的)

4, 所有的非静态成员加载完成之后,对所有非静态成员变量进行默认初始化

5, 所有非静态成员变量默认初始化完成之后,调用构造函数

6, 在构造函数入栈执行时,分为两部分:先执行构造函数中的隐式三步,再执行构造函数中书写的代码

6.1、隐式三步:

1,执行super语句

2,对开辟空间下的所有非静态成员变量进行显式初始化

3,执行构造代码块(注:代码块与非静态成员变量显示初始化无先后顺序,与代码顺序相关,如代码块在上,则先加载代码块)

    6.2、在隐式三步执行完之后,执行构造函数中书写的代码

7,在整个构造函数执行完并弹栈后,把空间分配的地址赋值给一个引用对象(对象的访问定位有句柄直接指针两种方式)

至此,Java堆中有一块内存新的内存 存储这个实例化的对象,对象里面包含了对象头、实例数据以及对齐填充。其中对象头又可以细分为两部分:

1、存储对象自身运行时数据:如哈希码、GC分代年龄、锁状态标志、线程持有的、偏向线程ID等信息

2、类型指针:即对象指向它的类元数据的指针,虚拟机通过这个来确定这个对象是哪个类的实例(比如是指向栈中的类声明)

实例数据是对象真正存储的有效信息。对齐填充没什么大用处。



用户头像

李博@Alex

关注

我们始于迷惘,终于更高的迷惘. 2020.03.25 加入

一个酷爱计算机技术、健身运动、悬疑推理的极客狂人,大力推荐安利Java官方文档:https://docs.oracle.com/javase/specs/index.html

评论

发布
暂无评论
JVM-技术专题-对象的实例化过程