三、JVM(HotSpot)Java对象

注:本博文主要是基于JDK1.7会适当加入1.8内容。

1、对象创建

第一步:虚拟机执行一条new指令时,首先会去检查这个指令的参数是否可以从常量池中获取到符号引用(再次回归到前两章提到的String.intern()用法),并且检查这个符号引用是否已加载、解析和初始化。如果没有,则必须先执行类加载过程。

第二步:类加载检查通过后,虚拟机为新对象分配内存。分配内存又分为两种方式:如果虚拟机中已使用内存和未使用内存分布均匀,那么新生对象只需要通过指针移动分配内存空间即可,这种方式叫做“指针碰撞(Bump the Pointer)”;如果虚拟机中已使用的内存和未使用的内存分布不均匀,则需要维护一张列表,记录哪些是可用内存,哪些是已使用内存,这种方法叫做“空闲列表(Free List)”。
选择哪种内存分配方式由虚拟机内存使用是否均匀来决定,而内存中使用与未使用内存分配是否均匀又取决于垃圾收集器是否带有压缩整理功能决定(Serial、ParNew等带有Compact过程的收集器采用指针碰撞方式分配,CMS基于Mark-Sweep的收集器采用空闲列表方式分配)

需要考虑一个问题是:对象创建过程是一个频繁调用的过程,有可能采用CAS配上失败重连保证操作原子性;内存分配动作划分在不同的空间中进行,每个线程都在Java堆中预先分配一块内存,称之为本地线程缓冲区(TLAB),哪个线程需要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定(参数为-XX:+/-UseTLAB),可参考volatile实现方式。

2、对象内存布局

对象在内存中存储的布局分为三块区域:对象头、实例数据、对齐填充。

  • 对象头:存储对象自身运行时数据(以32bit为例):哈希码25bit,GC分代年龄4bit,锁标志位2bit,固定位1bit,统称为Mark World。
对象内容 标志位 状态
对象哈希码,对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 膨胀(重量级锁指定)
空,不需要记录信息 11 GC标记
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

- 实例数据:对象中真正存储的有效信息。
- 对齐填充:非必须存在,也没有特殊含义,仅仅起着占位符的作用。HotSpot规定对象的起始地址为8字节的整数倍,就是对象的大小也必须是8字节的整数倍。而对象头为8字节的整数倍(32bit或64bit),当实例数据非8字节的整数倍时,需要对齐填充实现整个对象大小为8字节的整数倍。

3、对象访问定位

对象访问定位的方式分为两种:句柄和直接指针。
(1)句柄:Java堆中会划分一块内存作为句柄池,用来存放对象的句柄地址,句柄中包含对象实例数据指针(对象)和对象类型数据指针(方法区,JDK1.8后存放在Metaspace元数据空间)。类型指针是指对象指向它的类元素数据的指针,虚拟机通过这个指针确定这个对象是哪个类的实例。如果是数组对象,则必须还有一块用来记录数组长度的数据,虚拟机可以确定对象的大小,但是不能确定数组的大小,通过数组对象和数组长度则可以确定数组的大小。

过程:对象访问查找句柄池,找到该对象的句柄地址,对象实例数据指针获取实例数据,对象类型指针获取类型数据。总共有两次指针地址定位。

(2)直接指针:Java堆中存放对象地址,包含对象实例数据以及对象类型数据指针(方法区,JDK1.8后存放在Metaspace元数据空间)。

过程:对象访问查找对象地址,获得对象实例数据,通过对象类型数据指针获取对象类型数据。总共有一次指针定位。

HotSpot中采用的是直接指针方式,优势为对象会有频繁的访问操作行为,减少一次指针的定位累计下来是很大的性能提升。

猜你喜欢

转载自blog.csdn.net/zhangwei408089826/article/details/81628664
今日推荐