java堆对象分配、布局、访问全过程

PS:文章来源《深入理解java虚拟机第三版 2.3HotSpot虚拟机对象探秘》

1.对象的创建

java虚拟机收到一条字节码new指令时,会先检查该指令参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用的类是否已经被加载、解析和初始化,如果没有则必先执行类加载过程。

类加载完成后就是内存分配,内存分配实际上就是从堆中把一块确定大小的内存划分出来。若堆中内存是规整的,一边是已分配内存,一边是未分配内存,中间则放着一个指针作为分界点的指示器,那内存分配就是将指针挪动对象大小相等的距离,该分配方式称为“指针碰撞”。若堆总内存不是规整的,虚拟机则会维护一个列表记录哪些内存是可用的,在分配内存时找到一块较大的区域分配给对象并更新列表上的记录,该分配方式称为“空闲列表”。内存的规整与否与所采用的垃圾收集器是否带有空间压缩整理的能力决定。像Serial、ParNew带压缩整理过程的收集器就采用“指针碰撞”,CMS基于标记清除算法采用的就是“空闲列表”分配内存。

对象创建在虚拟机中是非常频繁的,因此需要处理并发问题。解决方案有两种:一种是采用CAS并重试的机制保证操作原子性;另一种是采用本地线程分配缓冲(TLAB),每个线程在java堆中都预先分配一快内存,哪个线程要分配内存就在哪个线程的缓冲区分配,用完了需要分配新缓存区时才需要锁定,虚拟机中可通过设置-XX:+/-UseTLAB来开启此功能。

内存分配完后就进行对象的初始化操作,将对象的字段初始化相对应的零值,保证字段在不赋初始值也可使用。

接下来还会对对象进行一些必要的设置,比如对象是哪个类的实例、如何找到类的元数据信息、对象的哈希码、对象的GC分代年龄、是否使用偏向锁等信息。这些信息都存储在对象的对象头中。

最后,new指令之后会执行Class文件中的<init>()方法对对象进行初始化,这样一个真正的对象才算完全构造出来。

2.对象的内存布局

在HotSpot虚拟机中,对象在堆中的存储布局分为三部分:对象头、实例数据和对齐填充。

①对象头包括两类信息。第一类是用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称为“Mark Word”。对象头的另外一部分是类型指针,即对象指向它的类型元数据的指针,java虚拟机通过这个指针来确定该对象是哪个类的实例,但并不是所有虚拟机实现都必须在对象数据上保留类型指针,此外,若对象是一个java数组,那对象头中还必须有一块用于记录数据组长度的数据。

②实例数据部分是对象真正存储的有效信息,即代码里所定义的各种字段的内容。

③对齐填充这部分不一定存在,没有什么特别的含义,只起着占位符的作用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,因此,如果对象实例数据部分没有对齐,则需要通过对齐填充来补全。

3.对象访问定位

对象创建完成后就是使用对象,java程序会通过栈上的reference数据来操作堆上的具体对象。至于如何访问,由虚拟机来定,主流的访问方式有使用句柄和直接指针两种。

①句柄访问的话,java堆中将可能划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄地址中包含了对象实例数据与类型数据各自具体的地址信息。

②直接指针访问的话,java堆中对象的内存布局就必须要考虑如何放置访问类型数据相关的信息,reference中存储的就是对象地址,如果只是访问对象本身,就不需要多一次间接访问的开销。

两种访问方式各有优势。句柄访问最大的好处就是reference中存储的是稳定的句柄,在对象被移动(垃圾收集时移动对象再正常不过)时只会改变句柄中的实例数据指针,而reference本身不需要被修改;直接指针访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,对象访问在java中非常频繁,因此积少成多也是极为可观的执行成本。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值