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、类型指针:即对象指向它的类元数据的指针,虚拟机通过这个来确定这个对象是哪个类的实例(比如是指向栈中的类声明)
实例数据是对象真正存储的有效信息。对齐填充没什么大用处。
评论