今天我们一起看一下HotSpot虚拟机中的对象。
对象的创建(以 new 关键字为例)
创建过程
- Java 虚拟机遇到字节码new指令,首先检查new指令参数是否能够在常量池中定位到一个类的符号引用
如果是,继续下一步
如果否,执行类加载过程
如果是,检查这个符号引用代表的类是否已经被加载、解析和初始化
如果否,执行类加载
- 虚拟机为新生对象分配内存(对象所需内存大小在类加载完成后即可确定)
- 虚拟机将分配的内存空间(不包括对象头)初始化为零值。
- 虚拟机对对象进行必要设置,比如设置对象头信息:
- 对象是哪个类的实例
- 如何找到类的元数据信息
- 对象的哈希码
- 对象的 GC 分代年龄
- new指令之后会接着执行<init>()方法,按照程序员意愿初始化对象
内存分配
- 内存分配算法:拟机为新生对象分配内存有指针碰撞和空闲列表两种方式,具体选择哪种,取决于垃圾收集器是否带有空间压缩整理的能力。Serial、ParNew 带压缩整理,采用指针碰撞;CMS 基于清除算法,采用空闲列表。
指针碰撞 (Bump The Pointer):堆内存绝对规整,已使用的在一边,未使用的在另外一遍,中间通过指针作为分界点指示器,分配内存即移动指针。
空闲列表 (Free List):堆内存不规整,已使用与未使用相互交错,需要维护一个列表,记录哪些内存块可用,分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表记录。
- 线程安全问题:创建对象比较频繁,需要保证线程安全,避免多个对象分配了相同的内存区域,一般是两种方式:
同步处理:虚拟机采用CAS+失败重试方式保证更新操作的原子性
本地线程分配缓冲:把内存分配的动作按照线程划分在不同空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓冲 (Thread Local Allocation Buffer, TLAB),哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲缓冲区用完了,分配新的缓冲区时才需要同步锁定。是否使用 TLAB,可以通过参数-XX:+/-UseTLAB参数设定。
对象的内存布局
- 对象头 (Header)
用于存储对象自身运行时数据:哈希码 (Hash Code)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,长度在 32 位和 64 位虚拟机分别是 32 比特和 64 比特,官方称为 Mark Word。
类型指针,即对象指向它的类型元数据指针,Java 虚拟机通过这个指针来确定该对象是哪个类的实例。
如果对象是数组,还有一个数据记录数组长度
- 实例数据 (Instance Data):即程序代码里面定义的各种类型的字段内容。存储顺序受虚拟机分配策略 (-XX:FieldsAllocationStyle 参数)和字段在 Java 源码中定义顺序影响。HotSpot 虚拟机默认分配顺序为 longs/doubles、ints、shorts/charts、bytes/booleans、oops(Ordinary Object Pointers, OOPS),即相同宽度字段被分配到一起存放,在满足这个前提条件情况下,在父类中定义的变了会出现在子类之前。如果 HotSpot 虚拟机的+XX:CompactFields 参数设置为 true,子类中较窄的变量也允许插入父类变量的空隙中,以节省空间。
- 对齐填充 (Padding):占位符作用。HotSpot 虚拟机的自动内存管理系统要求对象起始地址必须是 8 字节的整数倍。
对象的访问定位
Java 程序会通过栈上的 reference 数据来操作堆上的具体对象,主流的访问方式主要有使用句柄和直接指针两种:
- 使用句柄:Java 堆中将可能会划分出一块内存来作为句柄池,reference 中存储的是对象的句柄地址,句柄中包含了对象实例数据和类型数据各自的具体地址信息。好处是解耦,reference 中存储的是稳定句柄地址,在对象被移动(垃圾回收等)时只会改变句柄中实例数据指针,而 reference 本身不需要修改。
- 直接指针:Java 堆中的对象布局必须考虑如何放置访问类型数据的相关信息,reference 中存储的是对象地址。好处是速度快,节省一次指针定位时间开销,HotSpot 主要使用直接指针。
使用句柄
直接指针