[深入理解Java虚拟机]虚拟机中的对象

对象的创建

当虚拟机遇到一条new指令时:
  1. 首先将去检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并且检查这个符号引用代表的类是否已被加载、解析、和初始化过。如果没有,那必须先执行相应的类加载过程。
  2. 在类加载检查通过后,接下来虚拟机将为新生对象分配内存,对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务等同于把一块确定大小的内存从Java堆中划分出来。
    1. 如何划分可用空间:
      1. 假设Java堆中内存时绝对规整的,所有用过的内存都放在一边,空闲的内存放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump the Pointer)
      2. 如果Java堆中内存并不是规整的,已使用的内存和空闲内存相互交错,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,这种分配方式称为“空闲列表”(Free List)
      3. 选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定
    2. 解决并发情况下线程安全问题:
      1. 一种是对分配内存空间的动作进行同步处理——实际上虚拟机采用CAS配上失败重试的方式保证更新操作的原子性
      2. 另一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在Java堆中预先分配一小块内存,称为本地线程分配缓冲(Thread Local Allocation Buffer, TLAB)。哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完并分配新的TLAB时,才需要同步锁定。虚拟机是否使用TLAB,可以通过-XX:+/-UseTLAB参数来设定
  3. 内存分配完成后,虚拟机需要将分配到的内存空间都初始化为零值(不包括对象头),如果使用TLAB,这一工作过程也可以提前至TLAB分配时进行。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。
  4. 接下来,虚拟机要对对象进行必要的设置,即设置对象的对象头(Object Header)。将例如类类型、元数据信息、对象的哈希码、对象的GC分代年龄等信息存放进对象的对象头中。
  5. 执行new指令之后会接着执行<init>方法,把对象按照意愿进行初始化。

总结

  1. 检查这个符号引用代表的类是否已被加载、解析、和初始化;
  2. 为新生对象分配内存;
  3. 初始化内存空间;
  4. 设置对象头,存放类信息;
  5. 执行<init>方法

对象的内存布局

  对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、和对齐填充(Padding)
  1. HotSpot虚拟机的对象头包括两部分信息:
    1. 一部分是用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为32bit和64bit,官方称它为“Mark Word”。它被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的信息,它会根据对象的状态复用自己的存储空间。
    2. 对象头的另外一部分是类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针。
    3. 另外,如果对象是一个Java数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中却无法确定数组的大小。
  2. 实例数据部分是对象真正存储的有效信息,也是在程序代码中所定义的各种类型的字段内容。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、int、shorts/chars、bytes/boolean、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那么子类之中较窄的变量也可能会插入到父类变量的空隙之中
  3. 对齐填充部分仅仅起占位符的作用。因为HotSpot VM要求对象的大小必须是8字节的整数倍。因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

总结

对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)、和对齐填充(Padding)
1. 对象头中一部分存储对象自身的运行时数据;另一部分是类型指针,指向元数据;
2. 实例数据存储对象的有效信息,即各种类型的字段内容;
3. 对齐填充部分仅仅是占位符

对象的访问定位

对象访问方式取决于虚拟机实现而定。目前主流的访问方式有使用句柄直接指针两种。
1. 句柄访问:Java堆中将会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自的具体地址信息
这里写图片描述
2. 直接指针访问:Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,而reference中存储的直接就是对象地址
这里写图片描述
3. 优势对比:
  1. 使用句柄来访问的最大好处就是reference中存储的是稳定的句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中实例数据指针,而reference本身不需要修改;
  2. 使用直接指针访问方式的最大好处就是速度更快,它节省了一次指针定位的时间开销,由于对象的访问在Java中非常频繁,因此这类开销积少成多后也是很大的执行成本。
  3. HotSpot中是使用直接指针访问的。

猜你喜欢

转载自blog.csdn.net/vi_nsn/article/details/78907396