HotSpot JVM 「02」Java Object Layout

331 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第15天,点击查看活动详情

01-Klass and OOP

HotSpot JVM 实现中用 Klass-OOP 模型来表示 Java 中的类和对象。

图 1. Klass 的继承体系

图 1. Klass 的继承体系

图 2. oopDesc 的继承体系

图 2. oopDesc 的继承体系

Klass 结构是 .class 文件的运行时结构; oopDesc 结构是 Object 对象的运行时结构。

oopDesc(对象头)中包含了两部分信息(更多详细的内容参考[1]):

  • mark word,存储了哈希码、锁信息、GC元数据等。
  • klass word,类信息。注:应该是指针,指向 Metaspace 中的类结构。
  • possible alignment paddings,非必须

对象头后存储的是对象的实例数据。

图 3. oopDesc 布局示意图

图 3. oopDesc 布局示意图

JVM 中的普通对象表示为 instanceOopDesc,数组对象表示为 arrayOopDesc。两者在结构上的区别是,后者多了 4 个字节的长度信息。

02-Java Object Layout

在 32/64 位机器上,mark word 是有区别的(更多详细信息[1]):

32 bits:
--------
hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)

64 bits:
--------
unused:25 hash:31 -->| unused_gap:1   age:4    biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused_gap:1   age:4    biased_lock:1 lock:2 (biased object)

图 4. 32 bits mark word 可能的分配情况及其含义 图 4. 32 bits mark word 可能的分配情况及其含义

图 5. 64 bits mark word 可能的分配情况及其含义

图 5. 64 bits mark word 可能的分配情况及其含义

  • 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 升级过程(不可降级)?更多详细参考[1]

    偏向锁获取:CAS 比较线程ID,同一线程再次获得锁的效率提高。

    偏向锁获取失败时,说明有其他线程加入抢锁的队伍,在到达 safepoint 后获得偏向锁的线程被挂起,判断锁对象是否处于锁定状态,并据此决定撤销偏向锁或升级为轻量级锁。

    轻量级锁(自旋锁):

借助工具查看 Java 对象内存布局?

  • HSDB [1]
  • jol [1, 2]
    • 示例

      public class SimpleInt {
          private int state;
      }
      
      # Running 64-bit HotSpot VM.
      # Objects are 8 bytes aligned.
      # Field sizes by type: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
      # Array element sizes: 8, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
      
      # ClassLayout.parseClass(SimpleInt.class).toPrintable()
      self.samson.example.jol.SimpleInt object internals:
       OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
            0    16        (object header)                           N/A
           16     4    int SimpleInt.state                           N/A
           20     4        (loss due to the next object alignment)
      Instance size: 24 bytes
      Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
      # ClassLayout.parseInstance(instance).toPrintable()
      self.samson.example.jol.SimpleInt object internals:
       OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
            0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
            4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
            8     4        (object header)                           88 13 1a 1d (10001000 00010011 00011010 00011101) (488248200)
           12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
           16     4    int SimpleInt.state                           0
           20     4        (loss due to the next object alignment)
      Instance size: 24 bytes
      Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
      

03-对象创建过程

在所有会创建对象的场景中,该类的某个特定构造器方法会被调用。

  1. 创建类的实例时,会在堆上开辟空间存放所有的 instance variables,包括其超类声明的实例变量(也包括被子类 hide 的变量)。
  2. 实例变量被初始化为默认值
  3. 在新创建对象的引用被返回之前,调用特定的 constructor(先执行 instance initializers 和 instance variable initializers)
class Super {
	Super() { printThree(); }
	void printThree() { System.out.println("three"); }
}
class Test extends Super {
	int three = (int)Math.PI; // That is, 3
	void printThree() { System.out.println(three); }
	public static void main(String[] args) {
		Test t = new Test();
		t.printThree();
	}
}
// 输出
// 0
// 3
  • 内存分配方式有哪些呢?

    若堆是规整的,即使用过得放在一边,空闲未用的放在一边,在两者之间存在一个指针,称为分界指示器。分配方式就是指示器向空闲部分移动一定的大小。这种方式称为指针碰撞

    若堆不是规整的,空闲内存块列表记录在空闲列表中,分配方式为从空闲列表中取合适的大小分配给对象。

  • 访问对象的方式有哪些呢?

    通过句柄访问,栈中对象的引用指向的是句柄地址,句柄包含了实例数据与数据类型信息。对象移动时更方便。

    直接访问,栈中对象的引用指向的是实例数据的地址。访问速度更高。

  • 如何判断对象已经死亡?

    引用计数,最直接,最简单,但不能解决循环引用问题,而且算法边界很多,不容易实现

    可达性分析,从 GC Root 开始,根据引用关系向下搜索。GC Root 包括:栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中 JNI 引用的对象、JVM 内部的引用例如 Class 对象、所有被 synchronized 持有的对象、JMX 内部中注册的回调等等。

  • 应用类型?更多信息参考 [1]

    强引用

    软引用

    弱引用

    虚引用

  • 如何判断一个类不再使用?同时满足以下三个条件:

    类的所有实例已被回收

    类的加载器已被回收(通常难以达成,除非特殊设计目的存在,例如OSGI、JSP的重加载等)

    类对应的 Class 对象不被应用,也不能通过反射访问这个类


历史文章推荐