0%

JVM对象探秘

对于JVM运行时区域有了一定了解以后,本文将更进一步介绍虚拟机内存中存储数据的细节信息,虚拟机(Hotspot)最大的一块内存是堆,在堆中存储的主要内容是对象,文章将探讨对象的创建、布局以及如何访问。

对象的创建

在java中对象的创建主要由以下几种:

创建方式 是否调用构造函数
new
使用Class的newInstance
使用Construct类的newInstance
使用clone方法
使用反序列化

下面new关键字为例,讲述JVM堆中对象实例的创建过程如下:

  1. 当虚拟机遇到一条new指令时,首先会检查这个指令的参数能否在常量池中定位一个符号引用。然后检查这个符号引用的类字节码对象是否加载、解析和初始化。如果没有,将执行对应的类加载过程。
  2. 类加载完成以后,虚拟机将会为新生对象分配内存区域,对象所需内存空间大小在类加载完成后就已确定。
  3. 内存分配完成以后,虚拟机将分配到的内存空间都初始化为零值。
  4. 虚拟机对对象进行一系列的设置,如所属类的元信息、对象的哈希码、对象GC分带年龄 、线程持有的锁 、偏向线程ID 等信息。这些信息存储在对象头 (Object Header)。

上述工作完成以后,从虚拟机的角度来说,一个新的对象已经产生了。然而,从Java程序的角度来说,对象创建才刚开始。因为还没有调用init方法,你可以简单的理解这个方法调用构造器来进行初始化。

在上面为新生对象分配内存这一步,主要就是从堆中划分出一个空间,来存储类的实例。主要由俩种方法:

  1. 指针碰撞:这里假设堆中内存是绝对完整的,所有用过的内存都放在一边,空闲的内存都放在另一边,中间放着一个指针作为分界点的指示器,所以需要分配的内存就是仅仅把分界点的指针往空闲内存的一边移动一段与对象大小相等的距离。
  2. 空闲列表:如果java堆中内存不是规整的,已使用的内存和空闲的内存相互交错,虚拟机需要创建空闲列表,记录哪些内存是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。

从上面我们可以看出如果垃圾收集器使用标记-整理和标记-复制算法,则可以使用指针碰撞模式。如果使用的标记-清除则使用的是空闲列表模式。

在内存分配过程中,除了上述划分可用空间之外,还有另外一个需要考虑的问题,是对象在创建过程中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,所以需要考虑分配的安全性。在java中解决的方案有俩种:

  1. 对分配内存空间的动作进行同步处理-实际上是通过CAS重试的方式来保证原子操作。
  2. 把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在java堆中预先分配一个小块内存,称为本地线程分配缓冲(TLAB)。哪个线程分配内存,就在线程的TLAB上分配。只有在TLAB用完并分配新的TLAB时,才需要同步锁定。可以减少并发的次数。

对象内存布局

在内存中存储的布局可以分为3块区域:对象头(header),实例数据(Instance Data)和对齐填充(Padding)对象头。整体结构如下图结构如下图

对象结构图

对象头

对象头也就是上图中橙色的部分,主要包含MarkWord、length(数组长度)和Pointer,

  • MarkWord:存储的是运行时数据,如哈希码、GC分代年龄、线程持有的锁类、偏向线程Id等信息;
  • Pointer:是指向该对象的元数据信息,即该对象的Class实例;
  • length:如果是非数组类型,对象头中不包含这部分数据,数组类型则存储的是数组长度

因此对象头会出现俩种情形

普通对象
普通对象

数组对象
数组对象

MarkWord

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄等,会根据对象运行时不通的状态来调整字段对应的含义,其实主要的状态转变是由于加锁引起的。

mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。

为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

各种运行时数据

其中各部分的含义如下:

  • lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。
    标记

  • bias_lock:对象是否启动偏向锁标记,只占1个二进制位。为1时表示对象启动偏向锁,为0时表示对象没有偏向锁。

  • age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

  • identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

  • thread:持有偏向锁的线程ID。

  • epoch:偏向时间戳。

  • ptr_to_lock_record:指向栈中锁记录的指针。

  • ptr_to_heavyweight_monitor:指向monitor对象(也称为管程或监视器锁)的起始地址,每个对象都存在着一个monitor与之关联,对象与其monitor之间的关系存在多种实现方式,如monitor对象可以与对象一起创建销毁或当前线程试图获取对象锁时自动生成,但当一个monitor被某个线程持有后,它便处于锁定状态。

64位下的标记字与32位的相似:
64

pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。

如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项+UseCompressedOops开启指针压缩,其中,oop即ordinary object pointer普通对象指针。开启该选项后,下列指针将压缩至32位:

  • 每个Class的属性指针(即静态变量)
  • 每个对象的属性指针(即对象变量)
  • 普通对象数组的每个元素指针

当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

length

如果对象是一个数组,那么对象头还需要有额外的空间用于存储数组的长度,这部分数据的长度也随着JVM架构的不同而不同:32位的JVM上,长度为32位;64位JVM则为64位。64位JVM如果开启+UseCompressedOops选项,该区域长度也将由64位压缩至32位。

实例数据

这部分空间时对象真正存储的而有效信息,也是在程序代码中所定义的各种类型的字段内存。无论是从父类继承下来的,还是子类定义的,都需要记录起来。这部分的存储顺序收到虚拟机分配策略参数和字段在Java源码中定义顺序的影响。Hotspot虚拟机默认的分配策略为long/double -> int/float -> short/char -> bytes/boolean -> reference(对象指针)。从分配策略可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件情况下,在父类定中定义的便令会出现在子类之前。如果CompactFields参数值为true(默认为true),那么子类中较窄的变量也可能会插入到父类变量的空隙之中。

对齐填充

HotSpot虚拟机要求每个对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(32位为1倍,64位为2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

对象的访问定位

Java程序需要通过JVM栈上的引用访问堆中的具体对象。对象的访问方式取决于JVM虚拟机的实现。目前主流的访问方式有句柄直接指针 两种方式。
这里补充一点定义:
指针: 指向对象,代表一个对象在内存中的起始地址。
句柄: 可以理解为指向指针的指针,维护着对象的指针。句柄不直接指向对象,而是指向对象的指针(句柄不发生变化,指向固定内存地址),再由对象的指针指向对象的真实内存地址。

  1. 句柄
    Java堆中划分出一块内存来作为句柄池,引用中存储对象的句柄地址,而句柄中包含了对象实例数据与对象类型数据各自的具体地址信息,具体构造如下图所示:
    句柄
    优势:引用中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而引用本身不需要修改。

  2. 直接指针
    如果使用直接指针访问,引用 中存储的直接就是对象地址,那么Java堆对象内部的布局中就必须考虑如何放置访问类型数据的相关信息。
    直接指针
    优势:速度更快,节省了一次指针定位的时间开销。由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是非常可观的执行成本。

参考

  1. 周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社
  2. JVM系列(三) - JVM对象探秘
  3. Java对象头与monitor