yeskery

Java 对象结构

对象结构

在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。下图是普通对象实例与数组对象实例的数据结构:

hotspot_instance_oop_desc

HotSpot虚拟机的对象头包括两部分信息:

  1. markword
    第一部分 markword,用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,这部分数据的长度在32位和64位的虚拟机(未开启压缩指针)中分别为 32bit 和 64bit,官方称它为“MarkWord”。

  2. klass
    对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

  3. 数组长度(只有数组对象有)
    如果对象是一个数组, 那在对象头中还必须有一块数据用于记录数组长度。

实例数据

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

对齐填充

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

对象大小计算

要点

  1. 在32位系统下,存放Class指针的空间大小是4字节,MarkWord是4字节,对象头为8字节。
  2. 在64位系统下,存放Class指针的空间大小是8字节,MarkWord是8字节,对象头为16字节。
  3. 64位开启指针压缩的情况下,存放Class指针的空间大小是4字节,MarkWord是8字节,对象头为12字节。 数组长度4字节+数组对象头8字节(对象引用4字节(未开启指针压缩的64位为8字节)+数组markword为4字节(64位未开启指针压缩的为8字节))+对齐4=16字节。
  4. 静态属性不算在对象大小内。

HotSpot对象模型

HotSpot中采用了OOP-Klass模型,它是描述Java对象实例的模型,它分为两部分:

  • 类被加载到内存时,就被封装成了 klass,klass 包含类的元数据信息,像类的方法、常量池这些信息都是存在 klass 里的,你可以认为它是 java 里面的 java.lang.Class 对象,记录了类的全部信息;
  • OOP(Ordinary Object Pointer)指的是普通对象指针,它包含 MarkWord 和元数据指针, MarkWord 用来存储当前指针指向的对象运行时的一些状态数据;元数据指针则指向 klass,用来告诉你当前指针指向的对象是什么类型,也就是使用哪个类来创建出来的;
  • 那么为何要设计这样一个一分为二的对象模型呢?这是因为 HotSopt JVM 的设计者不想让每个对象中都含有一个 vtable(虚函数表),所以就把对象模型拆成 klass 和 oop,其中 oop 中不含有任何虚函数,而 klass 就含有虚函数表,可以进行 method dispatch。

HotSpot 中,OOP-Klass 实现的代码都在 /hotspot/src/share/vm/oops/ 路径下,oop 的实现为 instanceOop 和 arrayOop,他们来描述对象头,其中 arrayOop 对象用于描述数组类型。

以下就是 oop.hhp 文件中 oopDesc 的源码,可以看到两个变量 _mark 就是 MarkWord,_metadata 就是元数据指针,指向 klass 对象,这个指针压缩的是 32 位,未压缩的是 64 位;

  1. volatile markOop _mark; //标识运行时数据
  2. union _metadata {
  3. Klass* _klass;
  4. narrowKlass _compressed_klass;
  5. } _metadata; //klass指针

一个 Java 对象在内存中的布局可以连续分成两部分:instanceOop(继承自 oop.hpp)和实例数据;

hotspot_instance_oop_hpp

上图可以看到,通过栈帧中的对象引用 reference 找到 Java 堆中的对象,再通过对象的 instanceOop 中的元数据指针 klass 来找到方法区中的 instanceKlass,从而确定该对象的类型。

下面来分析一下,执行 new A() 的时候,JVM 做了什么工作。首先,如果这个类没有被加载过,JVM 就会进行类的加载,并在 JVM 内部创建一个 instanceKlass 对象表示这个类的运行时元数据(相当于 Java 层的 Class 对象)。初始化对象的时候(执行 invokespecial A::),JVM 就会创建一个 instanceOopDesc 对象表示这个对象的实例,然后进行 Mark Word 的填充,将元数据指针指向 Klass 对象,并填充实例变量。

元数据—— instanceKlass 对象会存在元空间(方法区),而对象实例—— instanceOopDesc 会存在 Java 堆。Java虚拟机栈中会存有这个对象实例的引用。

成员变量重排序

为了提高性能,每个对象的起始地址都对齐于8字节,当封装对象的时候为了高效率,对象字段声明的顺序会被重排序成下列基于字节大小的顺序:

  1. double (8字节) 和 long (8字节)
  2. int (4字节) 和 float (4字节)
  3. short (2字节) 和 char (2字节):char在java中是2个字节。java采用unicode,2个字节(16位)来表示一个字符。
  4. boolean (1字节) 和 byte (1字节)
  5. reference引用 (4/8 字节)
  6. <子类字段重复上述顺序>

我们可以测试一下 java 对不同类型的重排序,使用 jdk1.8,采用反射的方式先获取到 unsafe 类,然后获取到每个 field 在类里面的偏移地址,就能看出来了。

测试代码如下:

  1. import java.lang.reflect.Field;
  2. import sun.misc.Contended;
  3. import sun.misc.Unsafe;
  4. public class TypeSequence {
  5. @Contended
  6. private boolean contended_boolean;
  7. private volatile byte a;
  8. private volatile boolean b;
  9. @Contended
  10. private int contended_short;
  11. private volatile char d;
  12. private volatile short c;
  13. private volatile int e;
  14. private volatile float f;
  15. @Contended
  16. private int contended_int;
  17. @Contended
  18. private double contended_double;
  19. private volatile double g;
  20. private volatile long h;
  21. public static Unsafe UNSAFE;
  22. static {
  23. try {
  24. @SuppressWarnings("ALL")
  25. Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
  26. theUnsafe.setAccessible(true);
  27. UNSAFE = (Unsafe) theUnsafe.get(null);
  28. } catch (Exception e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. public static void main(String[] args) throws NoSuchFieldException, SecurityException{
  33. System.out.println("e:int \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("e")));
  34. System.out.println("g:double \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("g")));
  35. System.out.println("h:long \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("h")));
  36. System.out.println("f:float \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("f")));
  37. System.out.println("c:short \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("c")));
  38. System.out.println("d:char \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("d")));
  39. System.out.println("a:byte \t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("a")));
  40. System.out.println("b:boolean\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("b")));
  41. System.out.println("contended_boolean:boolean\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("contended_boolean")));
  42. System.out.println("contended_short:short\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("contended_short")));
  43. System.out.println("contended_int:int\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("contended_int")));
  44. System.out.println("contended_double:double\t"+UNSAFE.objectFieldOffset(TypeSequence.class.getDeclaredField("contended_double")));
  45. }
  46. }

以上代码运行结果如下:

  1. e:int 12
  2. g:double 16
  3. h:long 24
  4. f:float 32
  5. c:short 38
  6. d:char 36
  7. a:byte 40
  8. b:boolean 41
  9. contended_boolean:boolean 170
  10. contended_short:short 300
  11. contended_int:int 432
  12. contended_double:double 568

除了 int 字段跑到了前面来了,还有两个添加了 contended 注解的字段外,其它字段都是按照重排序的顺序,类型由最长到最短的顺序排序的;

对象头对成员变量排序的影响

有的童鞋疑惑了,为啥 int 跑到前面来了呢?这是因为 int 字段被提升到前面填充对象头了,对象头有 12 个字节,会优先在字段中选择一个或多个能够将对象头填充为 16 个字节的 field 放到前面,如果填充不满,就加上 padding,上面的例子加上一个 4 字节的 int,正好是 16 字节,地址按 8 字节对齐;

扩展contended对成员变量排序的影响

那么 contended 注解呢?这个注解是为了解决 cpu 缓存行伪共享问题的,cpu 缓存伪共享是并发编程性能杀手,不知道什么是伪共享的可以查看我前面写的 LongAdder类的源码解读 或者《java 中的锁 – 偏向锁、轻量级锁、自旋锁、重量级锁》这篇文章都有讲到,加了contended 注解的字段会按照声明的顺序放到末尾,contended 注解如果是用在类的 field 上会在该 field 前面插入 128 字节的 padding,如果是用在类上则会在类所有 field 的前后都加上 128 字节的 padding。

本文转载自:https://blog.csdn.net/zqz_zqz/article/details/70246212

评论

发表评论 点击刷新验证码

提示

该功能暂未开放