JVM笔记:虚拟机对象探秘

126 阅读5分钟

虚拟机对象探秘

1. 对象的创建

1.1 类加载检查

当Java虚拟机遇到一条字节码new指令时,首先去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已经被加载、解析和初始化过。

1.2 对象分配内存

我们根据内存是否规整有两种方法去分配

  1. 内存规整的情况下,所有使用过的内存空间都被放在一边,空闲的内存被放在另一边,中间有一个指针作为分界点的指示器。当需要内存分配的时候只需要将指针向空间内存挪动一段大小与对象相等的距离即可。这种分配方式称为指针碰撞。通常在复制清除算法、标记整理法都可以让内存规整。
  2. 内存不规整的情况下,Java堆会维护一个列表,上面记录哪些内存快可用和大小。当需要分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。这种分配方法称为空闲列表。标记清除法就会出现内存碎片,让内存不规整。

在内存分配的时候,除了是否规整之外的,在高并发的情况下,可能会出现线程安全问题。

解决这个问题由两个可选方案

  1. CAS+失败重试:失败则重试,一直到成功为止的方式保证更新操作的原子性
  2. 本地线程分配缓冲:为每个线程在Java堆上预分配一小块内存。每个线程使用内存的时候优先使用预分配内存。如果预分配内存不足则使用CAS+失败重试的方法重新获取一块预分配内存。

1.3 对象属性初始化零值

此步操作保证了对象的实例字段在Java代码中可以不赋值直接使用,使程序能访问这些字段的数据类型所对应的零值。

1.4 对象头设置

对象头中包含了:该对象是哪个类的实例、类的元数据、对象的哈希码、对象的GC分代年龄。还会根据是否使用偏向锁、轻量锁、重量锁对对象头有不同的设置方式。

1.5 调用构造方法

从虚拟机视角而言,对象已经创建了。对于Java程序而言,对象创建才刚刚开始。这一步骤会调用程序员设置的构造方法,对对象进行初始化,这样一个真正的对象才算被完全构造出来。

对象创建过程

2. 对象的内存布局

对象在Java堆中的布局可以划分为三个部分:对象头、实例数据、对齐填充

2.1 对象头

对象头包含了两部分信息。

  • 第一部分用于存储对象自身运行时数据,如哈希码、GC分代年龄、锁状态表示、线程持有做、偏向线程ID、偏行时间戳等。这部分信息官方称为Mark Word,是一个有着动态定义的数据结构。会根据锁状态的不同,Mark Word也会改变,但是包含的信息量不变。
  • 第二部分是类型指针,即对象指向它的类型元数据的指针。Java虚拟机会通过这个指针来确定该对象是哪个类的实例。
  • (不一定存在)第三部分是记录数组长度的数据。因为虚拟机需要通过普通Java对象的元数据信息确认Java对象的大小,如果数组的长度不确定的,将无法通过元数据中的信息推断出数组的大小。(Java对象的元数据就是指对象头)

2.2 实例数据

实例数据部分是对象真正存储的有效信息,即我们在程序代码里面所定义的各种类型的字 段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来

2.3 对齐填充

对象的第三部分是对齐填充,这并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作 用。由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是 任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数(1倍或者 2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

3. 对象的访问定位

创建对象是为了使用对象,虚拟机栈会通栈上的reference数据来访问堆上的具体对象。主流的访问方式有使用句柄和直接指针两种

3.1 使用句柄

  • 如何定位

    Java堆中会划分出一块内存作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象示例数据与类型数据各自的具体地址。这种访问方式的优势是稳定句柄地址,在对象被移动的时候只用改变句柄中的示例数据指针,而reference本身不需要修改。

    使用句柄

3.2 直接指针

  • 如何定位

    直接指针访问,reference中存储的直接就是对象的地址,对象的元数据则通过对象头的类型指针来指向。这种方式的优势就是速度更快,不需要多一次简介访问的开销。

    直接指针