心安

深入理解Java虚拟机(三) -- HotSpot虚拟机对象探秘

字数统计: 1.7k阅读时长: 5 min
2019/03/04 Share

一、对象的创建

对象创建(不包括数组和Class对象)的过程主要分为以下6步:

  1. 判断对象所属的类是否已被加载

    虚拟机在遇到一条new指令时,首先将去检查这个指令的参数是否能在常量池中定位到这个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析和初始化过。如果没有,那必须先执行类加载过程。这一细节在上一篇文章 深入理解Java虚拟机(二) – 虚拟机类加载机制 对于类加载时机的相关内容中已经描述过了。

  2. 分配内存

    类加载检查通过后,接下来虚拟机将为新生对象分配内存。对象所需内存大小在类加载完成就可以完全确定,这一细节下面对象的内存布局中会详细讲解,所以说分配内存这一任务就只需要把确定大小的内存从java堆中划分出来。这样内存分配就和垃圾收集器是否带有压缩整理的功能有关了。有些垃圾收集器带有压缩整理功能,在清理掉“无用的对象”之后,自动整理剩余的对象到内存的一端,这样新生对象的内存直接在后面分配就可以了,这种方式被称为“指针碰撞”。而不具有压缩整理功能的垃圾收集器,对象在内存中是零散分布的,这样就需要一个表来记录哪些内存可用,为新生对象分配内存时只需要在可用的内存中找到足够大的内存进行分配即可,这种方式被称为“空闲列表”。

  3. 解决并发问题

    还需要考虑的问题是如何解决多线程并发创建的问题,可能出现A线程为对象分配内存,B线程同时也使用了这块内存区域的情况。解决这种问题有两种方案,一种是分配内存动作进行同步加锁,实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性(详细请搜索CAS自旋锁)。另一种方案是把内存分配的动作按照线程划分在不同的空间中进行,也就是每个线程都有自己的一亩三分地,分配内存都在自己的一亩三分地进行,这样就不会冲突,这种成为本地线程分配缓冲(Thread Local Allocation Buffer), TLAB)。

  4. 初始化

    内存分配完成,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头,下文详细介绍对象头),这一步保证了对象的实例字段在Java代码中可以不赋初始值就可以使用。

  5. 进行必要的设置

    接下来,虚拟机要对对象进行必要的设置,例如这个对象是哪个类的实例,如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息。这些信息都存放在对象头中,根据虚拟机的运行状态不同,如是否启用偏向锁等,对象头会有不用的设置方式。

  6. 执行< init>方法

    接下来执行< init>方法,把对象按照程序员的意愿进行初始化,这样一个可用的对象才算完成产生出来。

二、对象的内存布局

在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对其填充(Padding)。

  1. 对象头

    对象头包括了两部分信息:

    一部分用于存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称之为“Mark Word”。

    另外一部分是类型指针,也就是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

    另外,如果对象是Java数组,还需要有一块用于记录数组长度的数据。

  2. 实例数据

    接下来的实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。无论是从父类继承下来的,还是子类自己定义的,都需要记录起来。

  3. 对其填充

    第三部分对其填充不是必然存在,也没有特别的含义。仅仅是因为在HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。因此,如果对象的长度不是8的整数倍就需要一部分对其填充来进行补齐。

三、对象的访问定位

上面说了创建对象和对象的内存布局,所有的这些都是为了使用这个对象。如下代码:

1
Object obj = new Object();

后面的 new Object() 是真正的对象,它存在于java堆中,而前面的 obj 只是这个对象的引用,它存在于虚拟机栈中。我们使用对象的引用 obj 来操作对象,那么这个引用是如何定位到这个对象的呢?目前主流的方式有两种,一种是使用句柄、一种是直接指针。

  1. 使用句柄

    这种方式,Java堆上会划分出一块内存区域作为句柄池,reference(引用,即下图中的obj)中存储的就是对象的句柄地址,而句柄中包含了指向了对象实例数据和对象类型数据的“指针”。

  2. 直接指针

    如果使用直接指针的方式,那么Java堆中的对象的布局中就必须放置访问类型数据的相关信息,而reference中存储的就直接是对象的地址。

    这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动时只需要改变句柄中的实例数据的指针,而不需要改变reference存储的地址(在垃圾收集的过程中对象被移动是很常见的事情。而使用直接指针的访问方式的有事就是速度更快,它节省了一次指针定位的时间开销。就本文讨论的HotSpot虚拟机而言,使用的是第二种方式进行对象访问的。

原文作者:XinAnzzZ

原文链接:https://www.yuhangma.com/2019/jvm/2019-03-04-jvm-object/

发表日期:March 4th 2019, 12:00:00 am

更新日期:February 2nd 2021, 4:44:20 pm

版权声明:(转载本站文章请注明作者和出处 心 安 – XinAnzzZ ,请勿用于任何商业用途)

CATALOG
  1. 1. 一、对象的创建
  2. 2. 二、对象的内存布局
  3. 3. 三、对象的访问定位