前言
都说Java是基于对象编程,那么在本文我们主要探讨一下关于对象在Java虚拟机中的一些创建以及内存的布局。
1. 对象的创建
JVM创建对象可以用流程图简单表示:
在其中分配内存时牵涉到JVM对空间的分配策略,这取决于Java堆中内存是否规整,针对此JVM有两种不同的策略:
- 指针碰撞:在Java堆中内存如果是绝对规整的,被使用过的内存放在一边,空闲的放在另一边,中间放着一个指针作为分界指示器,分配内存就是把指针向空闲方向挪动一段与对象大小相等的距离。
- 空闲列表:如果 Java 堆内存不规整,虚拟机必须维护一个列表记录哪些内存可用,在分配时从列表中找到一块足够大的空间划分给对象并更新列表记录。
两种策略一眼便知道第一种在效率上比第二种会快很多,而堆内存空间是否工整这又取决于虚拟机采用的是哪种GC算法;例如Serial采用的则是指针碰撞,而CMS采用的则是空闲列表方式。
2. 对象的内存布局
在HotSpot虚拟机中,对象在堆内存空间中可以大致分为三个部分来描述,分别是:对象头,实例数据和对其填充。
对象头
对象头中包括对象的两类信息:
- 第一类信息用于存储对象运行时数据,如哈希码、GC 分代年龄、锁标志、偏向线程 ID 等。
- 第二类则是类型指针,是对象指向它的数据类型的指针,作用是能让JVM通过这个指针来确定该对象是哪个类的实例。
实例数据
实例数据用来存储对象的真正有效信息,比如我们在代码中所定义的各种类型的字段内容。
对其填充
这部分并不必然存在,主要起占位符作用。
主要由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
3. 如何访问到对象
在了解了如何创建对象和对象的内存布局后,那么就要开始访问对象了,对象创建在了堆上,而JVM怎么操作对象呢?答案是通过栈上reference来进行操作,主流访问方式有句柄访问和直接指针访问两种:
- 使用句柄池访问:堆会划分出一块内存作为句柄池,reference 中存储对象的句柄地址,句柄包含对象实例数据与类型数据的地址信息。优点是 reference 中存储的是稳定句柄地址,在 GC 过程中对象被移动时只会改变句柄的实例数据指针,而 reference 本身不需要修改。如下图所示:
- 直接指针访问:
堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 存储对象地址,如果只是访问对象本身就不需要多一次间接访问的开销。优点是速度更快,节省了一次指针定位的时间开销,HotSpot 主要使用直接指针进行对象访问。如下图
小结:从两张图就能清晰看到直接指针访问对象是比句柄访问要快,能够节省一定时间开销;而句柄访问的好处在于当对象移动时(比如标记-整理算法进行GC时)只需要改变实例数据指针即可,不需要改变reference。所以说,两个方法各有各的好处,需要根据使用场景来进行选择。
周志明著《深入理解Java虚拟机》第三版