《深入理解 Java 虚拟机》HotSpot 虚拟机对象探秘

#java #jvm

本系列文章均摘自《深入理解 Java 虚拟机》第二版,略有删减、概括,主要是为了精简篇幅,方便阅读和以后查阅。推荐购买正版书籍,阅读完整内容。

本节将探讨 HotSpot 虚拟机在 Java 堆中对象分配、布局和访问的全过程。

2.3.1 对象的创建

JVM 遇到一条 new 指令时,会先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,然后检查这个符号引用代表的类是否已被加载、解析和初始化。如果没有,先执行相应的类加载(类加载后面会细说)。

类加载检查通过后,会为新生对象分配内存。假设 Java 堆的内存是绝对规整的,就是用过的内存在一边,没用过的在另一边,中间放着指针作为分界点指示器,那分配内存就是将指针向空闲的不分移动对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)。如果堆内存不是规整的,使用过的和未使用过的互相交错,JVM 就必须维护一个列表,记录哪些内存块是可用的,在分配的时候在记录中找到一块足够大的内存划分给新对象,并更新记录列表,这种分配方式叫“空闲列表”(Free List)。选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整由垃圾收集器是否带压缩整理功能决定。在使用 Serial、ParNew 等带 Compact 过程的收集器时,采用指针碰撞,而使用 CMS 这种基于 Mark-Sweep 算法的收集器时,采用空闲列表。

对象创建是非常频繁的,在并发情况下也并不是线程安全的,可能出现正在给对象 A 分配内存,指针还没来得及修改,对象 B 又同时使用了原来的指针分配内存的情况。解决这个问题有两种方案,一种是对分配内存空间的动作进行同步处理——虚拟机采用 CAS 配上失败重试的方式保证更新操作的原子性;另一种是把内存分配的动作按照线程划分在不同的空间内进行,每个线程在 Java 堆中预先分配一小块内存,称为本地线程分配缓存(Thread Local Allocation buffer,TLAB)。哪个线程要分配内存,就在哪个线程的 TLAB 上分配,只有 TLAB 用完并分配新的 TLAB 时,才需要同步锁定。JVM 上是否使用 TLAB ,可以通过 -XX:+/-UseTLAB 参数来设定。

内存分配完后,JVM 将分配到的内存空间初始化为零值(不包括对象头),如果使用 TLAB,初始化会提前至 TLAB 分配时进行。这样即使对象实例字段没有初始化也能访问到零值。

然后 JVM 对对象进行必要的设置,对象属于哪个类的实例、类的元数据信息、hashcode、GC 分代年龄等信息,存在对象头中。

以 JVM 的视角来看,一个对象已经产生了,但是从 Java 程序视角来看,对象创建才刚开始,执行 new 指令之后会执行 init 方法,把对象按照程序员的意愿进行初始化。

2.3.2 对象的内存布局

对象在内存中存储布局分为 3 块区域:对象头、实例数据、对齐填充。

JVM 对象头包括两部分信息,第一部分存储对象自身运行时数据,如 HashCode、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等。另一部分是类型指针,即对象指向它的类元数据的指针,JVM 通过这个指针来确定对象是哪个类的实例。

实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。

第三部分对齐填充不是必须的,也没有特别的意义。由于 HotSpot VM 的自动内存管理系统要求对象起始地址必须为 8 字节的整数倍,所以没对齐的时候需要对齐填充来补全。

2.3.3 对象的访问定位

建立对象是为了使用对象,Java 程序需要通过栈上的 reference 数据来操作堆上的具体对象。目前主流的访问方式有使用句柄和直接指针两种。

如果使用句柄访问的话,Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据的具体各自的地址信息。如图1所示。

如果使用直接指针访问的话,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,如图2所示。

这两种对象访问方式各有优势,使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项非常可观的执行成本。从上一部分讲解的对象内存布局可以看出,就虚拟机HotSpot而言,它是使用第二种方式进行对象访问,但在整个软件开发的范围来看,各种语言、框架中使用句柄来访问的情况也十分常见。

There are no comments on this post.