- 对象创建
虚拟机遇到一个new指令时,首先去,检查这个指令的参数是否在常量池中定位到一个雷的符号引用,并且检查这个符号引用代表的雷是否已被加载、解析、初始化过。
在类加载检查通过后,虚拟机将会为新对象分配内存,对象所需要的内存大小在加载后可以确定,为对象分配内存的任务就是把一块确定大小的内存从Java堆中划分出来。
- 假如堆时绝对规整的,所有用过的内存放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那么分配内存就仅仅是把那个指针指向空闲空间的那边挪动一段和对象大小相等的距离。这种分配内存的方式叫做指针碰撞(Bump the Pointer)
- 如果java堆并不是规整的,已使用的内存和空间内存相互交错,那就没办法简单的通过指针碰撞分配内存了,虚拟机必须维护一个表,记录哪些内存块是可用的,在分配内存时从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种方式叫做空闲类别free list。
具体选择哪种分配内存方式与GC有关,如果GC支持压缩,那么可以使用指针碰撞,否则就只能使用空闲列表了。使用Serial、ParNew等代用压缩过程的收集器时,系统采用指针碰撞,而使用CMS这种基于Mark-sweep算法的收集器时,通常采用空闲指针的方式。
除了如何划分可用内存之外,还有另一个需要考虑的问题时对象创建的是否频繁,因为堆内存时所有线程共享的,如果分配内存空间太频繁,会有竞争问题,虚拟机为了解决这个问题,使用了CAS配上失败重试的方式保证更新操作时原子性的。
另一种解决冲突的方式是把内存分配的动作按照线程划分在不同的空间中进行,即每个线程在Java堆上预先分配一块内存,这种方式成为Thread Local Allocation Buffer。
内存分配完毕后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果是TLAB方式,则初始化提前至TLAB分配时进行。这一步骤保证了对象的实例字段在Java代码中可以不赋初值就直接使用,程序访问这些字段的数据字段对应的零值。
- 对象内存布局
在Hotspot虚拟机中,对象在内存中存储的布局可以分为三部分:对象头、实例数据、对齐填充。
- 对象头用于存储两部分信息:第一部分用于存储对象自身的运行时数据,例如哈希吗、GC分代年龄、锁状态标识、线性持有的锁、偏向线程ID、偏向时间戳等,这部分可以成为Mark Word。
- 对象头的另一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象时哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,即查找对象的元数据信息并不一定要经过对象本身,如果一个对象是Java数组,那么对象头中还必须有一块记录数组长度的数据,因为虚拟机可以通过普通的Java对象的元数据中确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。
- 实例数据部分数据是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论从父类继承来的还是在子类中定义的,都需要记录起来。
- 第三部分对齐填充:并不是必然的,它仅仅起到占位符的作用,由于Hotspot虚拟机的自动内存管理系统要求对象起始位置地址必须是8字节的整数倍,因此如果对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
对象的访问定位
reference类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式取决于虚拟机实现。主流的定位对象实例方式有两种:使用句柄和直接指针方式。
- 使用句柄的话,会在Java堆中分配一块内存作为句柄池,而句柄中包含了对象的实例数据和类型数据各自的具体地址信息。
- 如果使用直接指针的方式,会在Java堆中存储对象实例的同时存储指向方法区的类型数据。
两种方式各有优势:使用句柄来访问的最大好处是referrence中存储的是稳定的句柄信息,在对象被移动时,只会改变句柄中的实例数据指针,而reference本身不会修改。
使用直接指针访问方式的最大好处就是速度快,它节省了一次指针定位的时间开销,对于对象的访问在Java中非常频繁,因此这类开销积少成多后会造成很客观的成本。