Java对象布局

发布于 2022-02-23  5.48k 次阅读


在JVM中对象在内存中的布局分为三块区域:

  • 对象头
  • 实例数据
  • 对齐数据

对象头分类:

  • instanceOopDesc:普通类型数据的对象头(重点)
  • arrayOopDesc:数组类型数据的对象头

一,对象头

普通对象头包含的部分: 

  • Mark Work
  • Klass pointer

      当一个线程尝试访问synchronized修饰的代码时,它首先要先获得锁,这个锁就存在于对象头里

Hotspot采用instanceOopDesc(普通对象的对象头)和arrayOopDesc数组对象的对象头),instanceOopDesc定义在Hotspot源码的instanceOop.hpp文件中,arrayOopDesc定义在arrayOop.hpp

instanceOop.hpp的源码:

//它继承了oopDesc父类
class instanceOopDesc : public oopDesc {
public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }


  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }


  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

oopDesc父类源码:这个oopDesc在oop.hpp文件中

class oopDesc {
  friend class VMStructs;
private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;


  // Fast access to barrier set.  Must be initialized.
  static BarrierSet* _bs;


public:
  markOop  mark() const         { return _mark; }
  markOop* mark_addr() const    { return (markOop*) &_mark; }
//省略其他代码

在普通实例对象中,oopDesc的定义包含两个成员,分别是_mark和_metadata:

  • _mark表示对象标记,属于markOop类型,也就是Mark World,它记录了对象和锁有关的信息
  • _metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针,_compressed_klass表示压缩类指针

对象头由两部分组成:

  • Mark Word 存储自身运行时数据
  • 指针

一,Mark Word

  Mark Word用于存储对象自身的运行时数据:

  1. 哈希码(HashCode)
  2. GC分代年龄
  3. 锁状态标志
  4. 线程持有的锁
  5. 偏向线程ID
  6. 偏向时间戳
  7. 等等

占用内存大小与虚拟机位长一致,Mark Word对应的类型是markOop,源码位于markOop.hpp中

//  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)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)
//
//  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
//  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
//  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
//  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

 上面是JVM源码中对Mark Word内容的注释,它分为32位和64位

在64位虚拟机下,Mark Word是64bit大小的,其存储结构如下:

二,Klass Pointer

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

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

1.每个Class的属性指针(即静态变量)

2.每个对象的属性指针(即对象变量)

3.普通对象数组的每个元素指针

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

对象头 = Mark Word + 类型指针(未开启指针压缩的情况下)

在32位系统中,Mark Word = 4bytes ,类型指针 = 4byte 

在32位系统中,Mark Word = 8bytes ,类型指针 = 8byte 

二,实例数据和对齐填充

实例数据:就是类中定义的成员变量

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

三,代码查看java对象布局

OpenJdk提供了专门看对象布局的依赖:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.9</version>
</dependency>

注:这个依赖超过0.9版本,打印的对象bin不是二进制线程而是16近制

例:查看一个对象的布局

① 实例对象

public class lockobj {
    int a;
}

② 打印实例对象布局

public class dome1 {
    public static void main(String[] args) {
        lockobj lockobj = new lockobj();
        //解析lockobj对象并返回字符串
        String s = ClassLayout.parseInstance(lockobj).toPrintable();
        //打印
        System.out.println(s);
    }
}

③ 结果(默认开启了指针压缩)

④ 关闭对象指针压缩(-XX:-UseCompressedOops)

例:在无锁的情况下,有31个bit表示hashcode

① 

public class dome1 {
    public static void main(String[] args) {

        lockobj lockobj = new lockobj();

        lockobj.hashCode();

        System.out.println(lockobj.hashCode());
        System.out.println(Integer.toHexString(lockobj.hashCode()));
        //解析lockobj对象并返回字符串
        String s = ClassLayout.parseInstance(lockobj).toPrintable();

        //打印
        System.out.println(s);
    }
}

② 结果


路漫漫其修远兮,吾将上下而求索