对象的创建过程
1、类加载
当虚拟机遇到一个 new 指令的时候,会先去检测这个指令的参数是否能定位到这个类的符号引用,并检查这个类是否被加载、解析、初始化过(在 JVM 的方法区中检查)。如果没有,则执行类加载(类加载机制)
2、内存分配
在类加载通过之后,虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定,相当于从 Java 堆中抽取一块内存出来;而根据内存的是否绝对规整,分为指针碰撞和空闲列表两种分配方式:
指针碰撞:假设 Java 堆中的内存是绝对规整的,分为空闲和非空闲两种,中间用一个指针当做划分界限的指示器;当一个新对象需要分配对象时,相当于把指针向空闲区域移动一段与对象大小相等的距离。
空闲列表:假设 Java 堆的内存不是绝对规整的,空闲和非空闲是相互交错的,那就需要一个 OopMap 列表,用来记录哪些内存块是可以用的,在对象分配内存时,划分一块大小相等的区域给对象,并更新这个列表
从上面的解释看,用哪种分配方式,是通过 Java 堆的内存块是否绝对规整决定的。
堆内存是否规整,主要是看 GC 回收了内存之后是否包含压缩或者整理功能.如果有,那么内存就比较规整.否则如果没有,创建对象就需要采用空闲列表的方式.
比如:serial,ParNew 等带有整理的收集器,可以使用指针碰撞.CMS 使用简单清除的算法,可以使用空闲列表.
但对象的创建是频繁的,在并发的情况,多线程不一定是安全的,即存在 A 对象在分配内存,指针还未来得及修改,B 对象也同时使用了原来的指针来分配对象。所以又衍生了两种解决办法,CAS+失败重试 和 TLAB 两种方式
CAS+失败重试:虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性 (关于 CAS 锁,是乐观锁的一种实现,解释起来也比较麻烦,
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、 在构造函数入栈执行时,分为两部分:先执行构造函数中的隐式三步,再执行构造函数中书写的代码:.1、隐式三步:1、执行 super 语句,2、对开辟空间下的所有非静态成员变量进行显式初始化3、执行构造代码块(注:代码块与非静态成员变量显示初始化无先后顺序,与代码顺序相关,如代码块在上,则先加载代码块),4、在隐式三步执行完之后,执行构造函数中书写的代码
7、在整个构造函数执行完并弹栈后,把空间分配的地址赋值给一个引用对象(对象的访问定位有句柄和直接指针两种方式)
至此,Java 堆中有一块内存新的内存 存储这个实例化的对象,对象里面包含了对象头、实例数据以及对齐填充。其中对象头又可以细分为两部分:
1、存储对象自身运行时数据:如哈希码、GC 分代年龄、锁状态标志、线程持有的、偏向线程 ID 等信息
2、类型指针:即对象指向它的类元数据的指针,虚拟机通过这个来确定这个对象是哪个类的实例(比如是指向栈中的类声明)
实例数据是对象真正存储的有效信息。对齐填充没什么大用处。
更多关于“Java培训”的问题,欢迎咨询千锋教育在线名师。千锋已有十余年的培训经验,课程大纲更科学更专业,有针对零基础的就业班,有针对想提升技术的好程序员班,高品质课程助理你实现java程序员梦想。