文章目录
1. 对象内存布局
在HotSpot虚拟机中(JDK默认的虚拟机),对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)
1.1 对象头
对象头包括两部分信息:Mark Word 和 类型指针:
Mark Word
- Mark Word 存储对象自身的运行时数据,包括哈希码、GC分代年龄、锁状态标志、偏向线程ID等;
- 对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构,以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存款空间;
Mark Word 在32位和64位虚拟机中的存储结构如下图:
- 32位虚拟机中的存储结构
- 64位虚拟机中的存储结构
类型指针
- 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;
- 类元数据存储在方法区中;
1.2 实例数据
- 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容;
- 无论是从父类继承下来的,还是在子类中定义的,都需要记录起来;
- 字段的存储顺序会受虚拟机分配策略参数和字段在Java源码中定义顺序的影响;
1.3 对齐填充
- 对齐填充不是必然存在的,仅仅起着占位符的作用;
- 由于HotSpot要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍;而对象头部分刚好是8字节的倍数,因此,当对象实例数据部分没有对齐时,需要通过对齐填充来补全;
2. 对象的创建
虚拟机遇到一条new
指令时,会依次执行类加载检查、内存分配、修改指针、空间初始化零值、对象设置,然后交给Java程序进行对象初始化;
2.1 类加载检查
类加载检查会检查这个指令的参数(类名)是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过,如果没有则必须先执行相应的类加载过程;
2.2 内存分配
在类加载检查通过后,虚拟机将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定;
为对象分配空间的任务,等同于把一块确定大小的内存从Java堆中划分出来,根据内存是否规整,分配方法有两种:指针碰撞和空闲列表
指针碰撞
假设Java堆中内存是绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器;那么分配内存时,就仅仅将这个指针向空闲空间挪到一段与对象大小相等的距离即可;
带压缩整理功能的垃圾收集器可以使用这种分配方式,如Serial
、ParNew
(垃圾收集器会在下一篇文章详解)
空闲列表
如果已使用的内存空间和空闲的内存空间是互相交错的,那么虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录;
在使用CMS
这种基于标记-清除算法的收集器时,通常采用空闲列表分配;
2.3 修改指针
内存分配成功后,需要修改对象的指针指向分配的位置,在并发的情况下不是线程安全的,可能出现正在给A对象分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况;
解决方案有两种:
- 同步处理:虚拟机采用CAS配上失败重试的方式保证更新操作的原子性;
- 本地线程分配缓冲:把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定;
- 虚拟机是否使用TLAB,可以通过
-XX:+/-UseTLAB
参数来设定;
2.4 空间初始化零值
内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就可以直接使用,程序能访问到这些字段的数据类型所对应的零值;
如果使用TLAB,这一工作也可以提前至TLAB分配时进行;
2.5 对象设置
接下来,虚拟机对对象进行必要的设置,即设置对象头的值;
例如,这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息;
2.6 对象初始化
对象设置完成后,对虚拟机来说,对象已经产生,可以交给Java程序,对Java程序来说,对象创建才刚刚开始,方法还没执行,所有的字段都还为零,所以执行完new
指令之后会接着执行方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生
3. 对象的访问定位
建立对象是为了使用对象,Java程序需要通过栈上的reference
数据来操作堆上的具体对象。
reference
类型在Java虚拟机规范中只规定了一个指向对象的引用,并没有定义这个引用应该通过何种方式去定位、访问堆中的对象的具体位置,所以对象访问方式也是取决于虚拟机实现的,目前主流的访问方式有两种:使用句柄和直接指针
3.1 使用句柄访问
如果使用句柄访问的话,Java堆中会划分出一块内存来存放句柄池,reference
中存储的就是对象的句柄地址,而句柄中包括了对象实例数据与类型数据各自具体的地址
优势:
reference
中存储的是稳定的句柄地址,在对象被移动时(垃圾收集时移动对象是非常普遍的行为)只会改变句柄中的实例数据指针,而reference
本身不需要修改;
3.2 使用直接指针访问
如果使用直接指针访问,那么Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference
中存储的直接就是对象地址;
HotSpot虚拟机中就是使用直接指针访问,访问对象类型数据的相关信息放在对象头里,由类型指针指向方法区中的对象类型数据(见第一小节)
优势:
- 速度快,节省了一次指针定位的时间开销
参考文献
- 《深入理解Java虚拟机》第2版,周志明 著;