1、对象的创建
在语言层面上,创建对象(例如克隆,反序列化)通常仅仅是一个new关键字而已。
在虚拟机中,对象(文中讨论的对象限于普通 Java 对象,不包括数组和 Class 对象等)的创建过程如下:
1.1、分配内存
空间分配的两种方式
- 指针碰撞:当已分配空间被集中存放,已分配和未分配空间使用一个指针来标记时,分配新的空间只需要移动该指针即可,此方法为指针碰撞。适用于 GC 算法会做 Compact 的情况。
- 空闲列表:当已分配的空间是分散存放时,虚拟机必须维护一个记录了哪些内存块是可用的列表,此为空闲列表,需要分配新空间时只需要从该列表中获取。
分配方式的选择取决于由Java堆是否规整,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。 因此,在使用 Serial、 ParNew 等带 Compact 过程的收集器时,系统采用的分配算法是指针碰撞,而使用 CMS 这种基于 Mark-Sweep 算法的收集器时,通常采用空闲列表。
解决空间分配线程安全问题的两种方式
- CAS方式失败重试:顾名思义,遇上分配时的线程冲突时,会再次进行空间分配直至成功。实现简单直观但是效率较低
- 空间划分:为每个线程分配单独的一块空间,该空间只用来给该线程做创建对象分配空间时使用,这单独的空间被称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)。当该线程的 TLAB 分配光了后,才需要同步锁定,效率较高。是否使用TLAB可以通过虚拟机参数 -XX:+/-UseTLAB 指定。
2、对象的内存布局
2.1、设置对象头
对象头包含两部分信息:
- 运行时数据(hash码、GC分代年龄、锁状态标志、 线程持有的锁、 偏向线程ID、 偏向时间戳等),这部分数据被称为 Mark Word。
- 类型指针,用来说明该对象是哪个类的实例。
2.2、设置对象实例数据
对象的实例数据主要是根据 JAVA 代码的编写生成的,包括父类在内的各种类型的字段,其存储顺序受虚拟机分配策略参数及代码中字段定义顺序的影响。
虚拟机安排字段的方式:
- 相同宽度的字段放在一起
- 父类的字段放在子类的前面
- 窄小的变量也会被安排在父类的字段空隙中(C++的内存安排规则,HotSpot VM是由C++语言编写)
2.3、填充字段
HotSpot VM要求对象的起始位置必须是8字节的整数倍,也就是说对象必须是8字节的整数倍,所以需要填充占位(这也是一句C++的规则来的)。
3、对象的访问
JAVA 程序访问对象需要通过栈上的 reference 数据操作堆上的具体对象。reference 对对象的访问方式主要为句柄和直接指针两种。
句柄访问
Java堆中将会划分出一块内存来作为句柄池,reference中存储的是对象的句柄地址,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
直接指针访问
reference中存储的直接就是对象地址
句柄访问方式的好处是 reference 中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要修改。
直接指针访问方式的好处是速度更快,它节省了一次指针定位的时间开销。
虚拟机Sun HotSpot 使用的是直接指针访问。