对象的创建
对象的创建过程如下图所示:
为对象分配内存
指针碰撞
假设 Java 堆中内存时完整的,已分配的内存和空闲内存分别在不同的一侧,通过一个指针作为分界点,需要分配内存时,仅仅需要把指针往空闲的一端移动与对象大小相等的距离。使用的 GC 收集器:Serial、ParNew,适用堆内存规整(即没有内存碎片)的情况下。
空闲列表
事实上,Java 堆的内存并不是完整的,已分配的内存和空闲内存相互交错,JVM 通过维护一个列表,记录可用的内存块信息,当分配操作发生时,从列表中找到一个足够大的内存块分配给对象实例,并更新列表上的记录。使用的GC收集器:CMS,适用堆内存不规整的情况下。
问题:到底应该选择哪种内存分配方式呢?
这个是由堆是否规整决定的,而堆是否规整是由垃圾回收策略决定的,如果垃圾回收器带有压缩整理的功能,能够实现堆的规整,就可以使用指针碰撞的分配方式,否则只能使用空闲列表的方式。
线程安全性问题
在高并发的情况下,同一时刻会有多个线程创建对象,分配内存,堆内存的分配存在线程安全性的问题。
问题:如何解决线程安全性的问题呢?
- 最简单的方案是实现线程同步,通过加锁的方式实现线程安全性,但是这种方案有一个问题就是执行效率比较低;
- 第二个方案是针对每个线程在堆内存中分配一块单独的区域,区域的大小可以通过虚拟机参数指定,这块区域称为本地线程分配缓冲(TLAB:Thread Local Allaction Buffer),通过为每一个线程单独分配内存的方式来避免线程安全性问题。当某个线程的本地线程分配缓冲已经满了后,可以再分配一块区域;
对象的结构
对象的结构主要包含如下三部分内容:
- Header:对象头
- InstanceData:实例数据
- Padding:对齐填充
对象头(Header)
对象头主要存储自身运行时数据、类型指针。
自身运行时数据
- 哈希值:Object 对象中 hashCode() 方法;
- GC 分代年龄:为分代收集算法所服务(分代好处:针对各个年龄代特点,选择适当的垃圾收集算法);
- 锁状态标志
- 线程持有的锁
- 偏向线程ID
- 偏向时间戳
类型指针
对象指向类的元数据的指针,虚拟机通过这个指针来确定对象是哪一个类的实例,并不是所有 JVM 实现都必须在对象数据上保留类型指针。
作为普通对象的对象头结构只有以上两种,但如果对象是一个数组,则对象头中还包含记录数组长度的数据。
实例数据(InstanceData)
数据的实例信息,真正存储对象有效信息的部分,我们所接触最多的部分。
不管是从父类继承中获得的还是从子类定义的实例信息,都被记录在其中。这部分的存储顺序由虚拟机的分配策略和字段在 Java 源码中定义的顺序决定,HotspotVM 默认策略是相同宽度的字段分配到一起,如 long/double 类型、short/char 分配到一起。在父类中定义的对象,可能出现在子类之前。
对齐填充(Padding)
这部分数据并不是必然存在的,它也没有特殊的含义,作用是作为一个占位符,用于填充内存。
为什么需要 Padding?
主要因为 HotspotVM 自动内存管理系统要求对象起始地址必须是8个字节的整数倍,也就是说对象的大小必须是8个字节的整数倍,而对象头部分正好是8个字节的整数倍。因此,对象实例数据部分如果没有对齐是就需要通过Padding 来进行填充。
对象的访问定位
Java 是通过虚拟机栈中的局部变量表中的 reference 数据来操作 Java 堆上的具体对象。但 reference 只是虚拟机规范中规定指向一个对象的引用,它并没有定义这个引用通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方法也取决于虚拟机的实现而定的。目前主流的访问方式有使用句柄和直接指针两种。
句柄访问
如果使用句柄访问,Java 堆中将会划分出一块内存作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据和对象类型数据的具体地址信息。实际上是采用了句柄池这样一个中间介质进行了两次指针定位,有效的避免了对象的移动或改变直接导致 reference 本身发生改变。句柄访问方式如下图所示:
使用句柄访问最大的好处就是 reference 中存储的是稳定的句柄地址,在对象回收过程中或者其它对象需要移动的时,只会改变句柄中的实例数据的指针,而 reference 本身不需要做任何修改。
直接指针访问
如果使用直接指针访问,那么 Java 堆对象的布局必须考虑如何放置访问类型的数据的相关信息,而 reference 中存储的直接就是对象地址,而不再是句柄地址信息,相当于在 reference 与对象地址信息直接少了句柄池这样一个中间地址,reference 中直接存储的就是对象地址。
这种定位方式也就导致了在对象被移动时,reference 本身必须发生改变。但是我们都知道,使用句柄访问方式时,相当于进行了两次指针定位,而直接指针访问方式恰好节省了这一次指针定位的时间开销,由于对象的访问在 Java 中非常的频繁,时间开销的减少也是一种可观的执行成本。例如,常见的 HotSpot 虚拟机就使用的是直接指针访问方式。
作者:殷建卫 链接:https://www.yuque.com/yinjianwei/vyrvkf/bdyoly 来源:殷建卫 - 架构笔记 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。