摘要:本文讨论HotSopt虚拟机中JAVA堆中对象的分配,布局以及访问的全过程。
创建对象
申请空间
本文的对象只讨论普通java对象,不包括数组和class对象。当虚拟机遇到一条new的指令时,首先去检查这个指令的参数能否在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载,解析和初始化过。没有就执行类加载过程。
在类加载检查通过之后,接下来虚拟机将为新生对象分配内存,对象所需内存大小在类加载完成后就可完全确定。而分配的方式有2种:
- 指针碰撞
java堆中内存规整,用过的和未用的放在2边,则只要把中间分界处的指针向着未分配的内存移动一段新对象大小的距离即可,非常简单。 - 空闲列表
JAVA堆中的内存不规整,就需要有一个列表记录哪些内存块可用,然后分配时从列表找一个足够大的未分配内存块分给新对象。
以上2种方式的关键在于是否规整,其实就取决于垃圾搜集器是否有压缩整理功能。关于垃圾收集算法,请看下篇。
划分空间还有一个关键问题就是并发的安全性。可能会在分配A对象内存的过程中,又出现了B对象也要同时分配,有2种方案:
- 对于分配内存采取同步
虚拟机采用CAS实现乐观锁外加失败重试的方式保证。 - 本地线程分配缓冲(TLAB)
按照线程预先分配,不同线程只能在自己的空间划分内存(TLAB),只有用完时才涉及到同步再去申请内存。虚拟机采用此方式需要通过XX:+/-UseTLAB参数来开启。
设置对象头部
在申请到空间后,需要将对象是那个类的,哈希码,GC分代年龄放在对象头。同时根据运行状态的不同如是否启动偏向锁等,对象头会有不同设置。
对象内存布局
在HotSpot虚拟机中,对象在内存中由对象头,实例数据和对齐填充组成。
对象头包含2部分:
第一部分存储对象自身运行时数据,如哈希码,GC分代年龄,锁状态,线程持有的锁,偏向线程ID,偏向时间戳等。
第二部分是类型指针,指向类元数据,这样就知道这个对象是哪个类的。另外如果对象是数组,还要有个记录数组长度的部分。
如果以上两部分大小不是8的整数倍,剩下的就是填充满足8的整数倍来对齐。
对象访问定位
JAVA程序需要通过栈上的reference数据来操作堆上的具体对象。主流的访问方式有两种
- 句柄
JAVA堆中划分出一个内存来作为句柄池,reference存储的就是对象的句柄地址,而句柄中包含了本对象的实例数据和类型数据各自的详细地址信息。
如下图: - 直接指针(Sun Hotspot使用此方式,性能更好)
reference直接存储对象的地址。
如下图